mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): give the Grok Intake its propose_draft tool (draft card)
The prompter prompt tells the model to call propose_draft when the spec is ready, but on Grok that tool didn't exist — so no draft chunk, no panel draft card, and the human couldn't launch a task from a Grok intake chat. - intake-tools.js: opencode plugin registering propose_draft via Hooks.tool; the execute() only ACKs — the driver (OpencodeServeSession.normalize -> _is_propose_draft -> _draft_from_tool_input) intercepts the tool CALL and emits the `draft` chunk the panel renders. - agent-grok-prompter.Dockerfile: bake the plugin, scoped to this image via ROBOCO_OPENCODE_EXTRA_PLUGINS (delivery roles never draft). - test: a propose_draft tool part normalizes to a draft chunk (not a tool_use). The live tool-call -> draft-card path is flagged UNVERIFIED-LIVE for the NAS.
This commit is contained in:
@@ -10,6 +10,14 @@
|
|||||||
|
|
||||||
FROM roboco-agent-grok
|
FROM roboco-agent-grok
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# The intake propose_draft tool plugin (the model calls it; the driver turns the
|
||||||
|
# call into the panel's draft card). Scoped to THIS image via
|
||||||
|
# ROBOCO_OPENCODE_EXTRA_PLUGINS so only the intake role carries it.
|
||||||
|
COPY docker/grok/intake-tools.js /app/opencode-plugins/intake-tools.js
|
||||||
|
ENV ROBOCO_OPENCODE_EXTRA_PLUGINS=/app/opencode-plugins/intake-tools.js
|
||||||
|
|
||||||
USER agent
|
USER agent
|
||||||
|
|
||||||
LABEL role="grok-prompter"
|
LABEL role="grok-prompter"
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// opencode plugin — the Intake interviewer's propose_draft tool, on Grok.
|
||||||
|
//
|
||||||
|
// Parity with the Claude Intake's SDK tool (roboco.agent_sdk.intake_driver
|
||||||
|
// .build_intake_options): the model calls propose_draft once the task spec is
|
||||||
|
// ready, and the driver (OpencodeServeSession.normalize_opencode_message ->
|
||||||
|
// _is_propose_draft -> _draft_from_tool_input) turns that tool call into the
|
||||||
|
// `draft` chunk the panel renders as the reviewable draft card.
|
||||||
|
//
|
||||||
|
// Without this, propose_draft is a tool the prompter prompt tells the model to
|
||||||
|
// call but that does not exist on Grok, so no draft card ever appears and the
|
||||||
|
// human can't launch a task from a Grok intake chat. The execute() only ACKs —
|
||||||
|
// the payload that matters is the tool-CALL input, which the driver intercepts.
|
||||||
|
//
|
||||||
|
// Loaded ONLY into the roboco-agent-grok-prompter image via
|
||||||
|
// ROBOCO_OPENCODE_EXTRA_PLUGINS (the one-shot delivery roles never draft).
|
||||||
|
//
|
||||||
|
// UNVERIFIED-LIVE: opencode's exact tool-call Part shape in the synchronous
|
||||||
|
// serve reply — confirm a Grok intake spec yields a draft chunk -> panel card
|
||||||
|
// on the NAS before relying on Grok intake.
|
||||||
|
|
||||||
|
import { tool } from "@opencode-ai/plugin";
|
||||||
|
|
||||||
|
export default async () => ({
|
||||||
|
tool: {
|
||||||
|
propose_draft: tool({
|
||||||
|
description:
|
||||||
|
"Submit the finished task draft for the human to review and confirm. " +
|
||||||
|
"Call this once the spec is complete. Pass a JSON object: title, " +
|
||||||
|
"objective, what_this_builds[], the_work[] ({team, summary, items}), " +
|
||||||
|
"notes[], acceptance_criteria[], team, scale, task_type, nature, " +
|
||||||
|
"estimated_complexity, priority.",
|
||||||
|
args: {
|
||||||
|
draft: tool.schema
|
||||||
|
.record(tool.schema.string(), tool.schema.any())
|
||||||
|
.describe("The task draft object"),
|
||||||
|
},
|
||||||
|
async execute() {
|
||||||
|
// The driver intercepts the tool CALL and emits the draft chunk; this
|
||||||
|
// handler only acknowledges so the model knows the draft landed.
|
||||||
|
return "Draft submitted — the human can review it.";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -51,6 +51,27 @@ def test_fenced_draft_in_text_becomes_draft_chunk() -> None:
|
|||||||
assert draft.data["title"] == "Add login"
|
assert draft.data["title"] == "Add login"
|
||||||
|
|
||||||
|
|
||||||
|
def test_propose_draft_tool_part_becomes_draft_chunk() -> None:
|
||||||
|
# The intake-tools.js propose_draft tool call (its input nested under
|
||||||
|
# `draft`) is intercepted into a draft chunk — NOT rendered as a tool_use —
|
||||||
|
# so the panel shows the draft card. This is the primary Grok-intake path.
|
||||||
|
chunks = normalize_opencode_message(
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"tool": "propose_draft",
|
||||||
|
"input": {"draft": {"title": "Add login", "team": "backend"}},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert "tool_use" not in _kinds(chunks)
|
||||||
|
draft = next(c for c in chunks if c.kind == "draft")
|
||||||
|
assert draft.data["title"] == "Add login"
|
||||||
|
assert draft.data["team"] == "backend"
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_part_skipped_but_turn_still_ends() -> None:
|
def test_unknown_part_skipped_but_turn_still_ends() -> None:
|
||||||
chunks = normalize_opencode_message({"parts": [{"type": "mystery", "x": 1}]})
|
chunks = normalize_opencode_message({"parts": [{"type": "mystery", "x": 1}]})
|
||||||
assert _kinds(chunks) == ["turn_end"]
|
assert _kinds(chunks) == ["turn_end"]
|
||||||
|
|||||||
Reference in New Issue
Block a user