feat(ui): Skill Builder built-in-tool awareness + author-not-role-play + gated scheduling (closes #270)#297
Conversation
…y; gated scheduling (closes #270) The Skill Builder was non-convergent and not built-in-aware: for "reply with the current time in German" it would either invent a redundant `brisbane_time` tool, offer a non-functional "conversational only" path (which makes the agent hallucinate the time), or role-play the answer and fabricate a time — instead of authoring a SKILL.md that calls the `datetime_now` built-in. Prompt changes (forge-ui/skill_builder_context.go): - New "## Built-in Tools" section listing the always-registered built-ins (datetime_now, web_search, http_request, json_parse, csv_parse, math_calculate, uuid_generate, schedule_set/list/delete/history) with a rule to PREFER them over a custom tool or prose, and never duplicate one (the brisbane_time anti-example). - No tool-less "conversational only" path for live-data requests — the agent can't know the time/web/API result without a tool call. - Role separation: AUTHOR a SKILL.md; never perform the behavior in chat or fabricate tool output; the skill goes in the `skill` field. - Scheduling: recognize scheduling intent → wire `schedule_set`; proactively ask ONLY for time/event-oriented skills (gated), always honor an explicit request. - Relaxed the `## Tool:` requirement: those are for CUSTOM tools only — a built-in-only skill has none. Added a worked built-in-only example (german-brisbane-time), the exact repro from #270. Tests: BuiltinAwareness, RoleSeparation, SchedulingGated, and BuiltinOnlySkillNeedsNoToolSection pin the new prompt contract. Docs: docs/ui/skill-builder-llm.md gains a "Built-in tool awareness" section.
initializ-mk
left a comment
There was a problem hiding this comment.
Review: Skill Builder built-in-tool awareness
Since a hardcoded "always available" tool list is only as correct as its match with reality, I verified every claim against the codebase:
datetime_now's documented args are exactly right —timezone(IANA, default UTC) andformat(rfc3339|unix|date|time|datetime) match the tool'sInputSchemaverbatim.schedule_set's cron syntax matches the runner's own scheduler system-prompt (5-field,@dailyaliases,@every <duration>), and the schedule tools are registered by the runner at startup.cli_executeis correctly excluded — conditionally registered (only when a skill declares bins), not inbuiltins.All().- The stacking claim is true: #276 merged with the
./forge-ui/...CI coverage, so these prompt tests genuinely run in CI.
Three minors, all inline. What's good: the role-separation and no-tool-less-path rules directly address each #270 failure mode; the worked example is the literal repro turned into the correct template (the strongest kind of prompt anchor); the scheduling gate prevents both failure directions and "writing 'runs daily' in prose schedules NOTHING" is exactly the blunt phrasing that works on models; the ## Tool: relaxation is scoped precisely; docs updated in the same PR.
Verdict
Merge-ready once the pending builds go green and the file_create omission is fixed (two-line change), with the drift-pin test as cheap insurance that keeps this PR's whole premise true over time. The schedule_set K8s note can be a docs ride-along or follow-up.
| - ` + "`" + `http_request` + "`" + ` — HTTP call to an allowed egress domain. Use to hit a REST API directly (no script needed for a simple call). | ||
| - ` + "`" + `json_parse` + "`" + ` / ` + "`" + `csv_parse` + "`" + ` — parse JSON / CSV payloads. | ||
| - ` + "`" + `math_calculate` + "`" + ` — evaluate an arithmetic expression. | ||
| - ` + "`" + `uuid_generate` + "`" + ` — generate a UUID. |
There was a problem hiding this comment.
Minor: file_create is missing from this list — and it IS in builtins.All().
The always-registered set is exactly: http_request, json_parse, csv_parse, datetime_now, uuid_generate, math_calculate, web_search, file_create. The list covers the first seven but omits file_create — the one tool wired into the loop's artifact-attachment pipeline. A user asking for a "generate a report file and attach it" skill will get a custom script scaffolded instead of the built-in that already delivers files to the channel correctly. One bullet fixes it.
| // advertise Forge's registered built-ins and prefer them over a custom tool | ||
| // or a tool-less behavior. Without this it invents redundant tools (e.g. | ||
| // brisbane_time) or offers a hallucinating "conversational only" path. | ||
| func TestSkillBuilderPrompt_BuiltinAwareness(t *testing.T) { |
There was a problem hiding this comment.
Minor (maintainability): pin the list against builtins.All() so it can't drift.
The prompt's tool list is hand-maintained prose in a Go string — the next builtin added to All() goes stale silently, recreating a softer #270 ("builder doesn't know tool X exists"). A ~6-line drift test here:
for _, tool := range builtins.All() {
if !strings.Contains(p, "`"+tool.Name()+"`") {
t.Errorf("prompt missing always-registered builtin %q", tool.Name())
}
}would have caught the file_create omission flagged on the context file. (The fuller alternative — generating the list dynamically from builtins.All() names + descriptions, with the curated arg notes layered on top — is also viable since forge-ui already depends on forge-core, but the drift test is the minimum for this PR.)
| - ` + "`" + `json_parse` + "`" + ` / ` + "`" + `csv_parse` + "`" + ` — parse JSON / CSV payloads. | ||
| - ` + "`" + `math_calculate` + "`" + ` — evaluate an arithmetic expression. | ||
| - ` + "`" + `uuid_generate` + "`" + ` — generate a UUID. | ||
| - ` + "`" + `schedule_set` + "`" + ` / ` + "`" + `schedule_list` + "`" + ` / ` + "`" + `schedule_delete` + "`" + ` / ` + "`" + `schedule_history` + "`" + ` — register / list / remove / inspect scheduled jobs. ` + "`" + `schedule_set` + "`" + ` takes a ` + "`" + `cron` + "`" + ` expression (5-field, ` + "`" + `@daily` + "`" + `/` + "`" + `@hourly` + "`" + `/…, or ` + "`" + `@every <duration>` + "`" + `) and a ` + "`" + `task` + "`" + `. Use for anything recurring or time-triggered — writing "runs every day" in prose schedules NOTHING; the agent must call ` + "`" + `schedule_set` + "`" + `. |
There was a problem hiding this comment.
Minor: "always available" over-promises for schedule_set on Kubernetes. Registration is unconditional, but on K8s deployments with the default scheduler.kubernetes.allow_dynamic: false, runtime CronJob creation by the LLM is denied — a skill wiring schedule_set authored by this builder silently won't work on a default K8s deploy. Not the builder's fault, but since the prompt now actively steers toward schedule_set, one sentence here (or in the docs section) noting the deployment gate would save operators a confusing debugging session.
…ule caveat (#297 review) - file_create was missing from the Built-in Tools list despite being in builtins.All() — a "generate a report file and attach it" skill would get a script scaffolded instead of the built-in that already delivers files to the channel. Added it. - Drift-pin test (TestSkillBuilderPrompt_ListsEveryDefaultBuiltin): iterates builtins.All() and asserts each name is advertised in the prompt, so a future default builtin can't silently go missing (would have caught file_create). - schedule_set "always available" over-promised on Kubernetes: dynamic calls are gated by scheduler.kubernetes.allow_dynamic (off by default). Noted in the prompt (steer the model to record it in ## Important Notes) and the docs.
|
Thanks — and appreciate the against-the-code verification of the tool list. All three minors addressed.
Drift-pin test. Added
Build / vet / test / lint clean; the four #270 tests plus the new drift-pin run in CI (forge-ui coverage from #276). |
initializ-mk
left a comment
There was a problem hiding this comment.
Re-review of e1e036c — all three findings resolved, each slightly better than asked
Finding 1 (file_create) — added with an accurate description that captures why it beats a script: "the runtime attaches it to the channel response. Use for 'generate a file / report and send it' needs instead of scaffolding a script." Docs list updated to match.
Finding 2 (drift-pin test) — implemented in exactly the suggested shape. TestSkillBuilderPrompt_ListsEveryDefaultBuiltin iterates builtins.All() and asserts each name appears backtick-wrapped in the prompt, with a comment recording that it caught the original file_create omission. The Test check passing on this commit confirms the pin holds against the real registry — and the docs now mention the pin exists, which keeps the doc claim honest too.
Finding 3 (K8s schedule_set gate) — addressed in both places, with a nice twist. The prompt bullet notes the allow_dynamic: true requirement AND instructs the model to record the caveat in the generated skill's ## Important Notes when the skill relies on scheduling — so the warning propagates into every authored skill, not just the builder's own context. The docs add the operator-facing version (won't self-register on a default K8s deploy unless the gate is enabled or the schedule is declared in forge.yaml).
Verdict
All findings resolved — merge-ready once the pending platform builds / integration checks go green (Lint, Test incl. the new drift test, and doc-link already pass). The drift test is the lasting value here: the prompt's built-in awareness can no longer silently rot as the registry grows.
Closes #270.
Problem
For a request like "when I ask the time, reply in German with Brisbane time" the Skill Builder produced one of three wrong outputs (non-deterministically):
brisbane_timecustom tool (whendatetime_nowalready exists).14:32) instead of authoring a SKILL.md.Root cause: the prompt wasn't aware of Forge's registered built-ins, offered a non-functional tool-less path, and conflated authoring with performing.
Fix (prompt engineering in
skill_builder_context.go)## Built-in Toolssection — lists the always-registered built-ins (datetime_now,web_search,http_request,json_parse,csv_parse,math_calculate,uuid_generate,schedule_set/_list/_delete/_history) and mandates preferring them, with thebrisbane_timeanti-example spelled out.skillfield.schedule_set; proactively ask only for time/event-oriented skills (gated to avoid interview fatigue), always honor an explicit request.## Tool:requirement — those are for custom tools only; a built-in-only skill has none (the validator agrees — a missing## Tool:is a warning, not an error).german-brisbane-time) — the exact Skill Builder: non-convergent + not builtin-aware — role-plays/hallucinates or scaffolds redundant tools instead of authoring a SKILL.md #270 repro, now the correct template.Acceptance criteria (from the issue + owner comments)
schedule_seton scheduling intent; proactive ask gated to time/event-oriented skillsdocs/ui/skill-builder-llm.md)Tests
TestSkillBuilderPrompt_BuiltinAwareness,_RoleSeparation,_SchedulingGated,_BuiltinOnlySkillNeedsNoToolSectionpin the new contract; existing convergence / install-recipe / structured-output / correctness assertions still pass.Note: this rides on the
forge-uiCI coverage added in #276, so these tests actually run in CI. Build / vet / test / lint clean locally.