Skip to content

feat(ui): Skill Builder built-in-tool awareness + author-not-role-play + gated scheduling (closes #270)#297

Merged
initializ-mk merged 2 commits into
mainfrom
feat/skill-builder-tool-awareness
Jul 13, 2026
Merged

feat(ui): Skill Builder built-in-tool awareness + author-not-role-play + gated scheduling (closes #270)#297
initializ-mk merged 2 commits into
mainfrom
feat/skill-builder-tool-awareness

Conversation

@initializ-mk

Copy link
Copy Markdown
Contributor

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):

  1. Invented a redundant brisbane_time custom tool (when datetime_now already exists).
  2. Offered a "conversational only" path — a tool-less skill that makes the agent hallucinate the time.
  3. Role-played the answer in chat and fabricated a time (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 Tools section — 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 the brisbane_time anti-example spelled out.
  • No tool-less path for live data — the agent can't know the time / live web / an API result without a tool call, so there's no valid "conversational only" skill for those.
  • Role separation — the builder AUTHORS a SKILL.md, never performs the behavior in chat, never fabricates 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 to avoid interview fatigue), always honor an explicit request.
  • Relaxed ## 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).
  • Worked built-in-only example (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)

  • Built-in-tool awareness — match intent to a built-in before a custom tool / tool-less path
  • No non-functional "conversational only" path for live-data requests
  • Role separation / convergence — author, never role-play or fabricate
  • Scheduling: wire schedule_set on scheduling intent; proactive ask gated to time/event-oriented skills
  • Docs updated (docs/ui/skill-builder-llm.md)

Tests

TestSkillBuilderPrompt_BuiltinAwareness, _RoleSeparation, _SchedulingGated, _BuiltinOnlySkillNeedsNoToolSection pin the new contract; existing convergence / install-recipe / structured-output / correctness assertions still pass.

Note: this rides on the forge-ui CI coverage added in #276, so these tests actually run in CI. Build / vet / test / lint clean locally.

…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 initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 righttimezone (IANA, default UTC) and format (rfc3339|unix|date|time|datetime) match the tool's InputSchema verbatim.
  • schedule_set's cron syntax matches the runner's own scheduler system-prompt (5-field, @daily aliases, @every <duration>), and the schedule tools are registered by the runner at startup.
  • cli_execute is correctly excluded — conditionally registered (only when a skill declares bins), not in builtins.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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment thread forge-ui/skill_builder_context.go Outdated
- ` + "`" + `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` + "`" + `.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@initializ-mk

Copy link
Copy Markdown
Contributor Author

Thanks — and appreciate the against-the-code verification of the tool list. All three minors addressed.

file_create omission (the blocking one). Added it to the Built-in Tools list — "create a file (report/export); the runtime attaches it to the channel response; use instead of scaffolding a script." Good catch: it's in builtins.All() and is exactly the tool a "generate a file and send it" skill needs.

Drift-pin test. Added TestSkillBuilderPrompt_ListsEveryDefaultBuiltin — iterates builtins.All() and asserts each name appears in the prompt, so the hand-maintained list can't silently go stale (it now fails if a new default builtin isn't advertised — exactly what would have caught file_create). Went with the drift test rather than fully generating the list, since the curated arg-notes (timezone/format, cron syntax) are the load-bearing part and worth keeping hand-written.

schedule_set K8s caveat. You're right that "always available" over-promised — verified the gate: scheduler_k8s_backend.go rejects SourceLLM Set calls when allow_dynamic is false (default). Added a note in the prompt (steering the model to record it in ## Important Notes for scheduling skills) and in the docs section.

Build / vet / test / lint clean; the four #270 tests plus the new drift-pin run in CI (forge-ui coverage from #276).

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@initializ-mk initializ-mk merged commit 6d97fb7 into main Jul 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill Builder: non-convergent + not builtin-aware — role-plays/hallucinates or scaffolds redundant tools instead of authoring a SKILL.md

1 participant