mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(grok): deliver intake draft via the relay + correct opencode-mechanism docs
Live end-to-end verification (opencode 1.17.8 + grok-build-0.1) of the WHOLE
integration, then fixes for what it surfaced:
1) Intake draft card (FUNCTIONAL): opencode's synchronous serve reply
(POST /session/:id/message) returns only [step-start, text, step-finish] — it
does NOT include tool-call parts, so the driver could never extract the
propose_draft draft. intake-tools.js now POSTs the draft straight to the
prompter-live relay (/api/prompter/live/{session}/events, the same endpoint
the driver's relay sink uses), so the panel renders the card regardless.
Verified live: grok calls propose_draft -> the relay receives the draft.
2) Correct misattributed opencode "bugs" (DOCS): earlier comments asserted as
general opencode behavior that a provider.xai block / npm override / config
plugin:-array "break" registration. Re-testing showed those were artifacts of
a PROJECT-level .opencode/opencode.json; from the GLOBAL config (which
opencode_config writes) the built-in provider, model resolution, the plugin
array AND the auto-discovery dir all work, and MCP gateway verbs register
(delivery agents verified). Reframed the comments as design choices (built-in
provider + XAI_API_KEY env + plugins baked in the auto-discovery dir with
named exports) and dropped the false claims.
3) Reasoning --variant: passing it does not error, but whether opencode applies a
named reasoning variant to grok-build-0.1 (no provider-defined variants) is
UNVERIFIED — comment softened from a "~54% cut" claim to best-effort,
measure-on-NAS.
Verified live this session: one-shot delivery (model + MCP verbs + plugins +
hooks), secretary tools (read_company_state + submit_directive -> backend with
token), intake draft (relay), grok built-in-provider tool-calling. Remaining
NAS-only: full container assembly (SDK :9000 startup, entrypoint hooks, 429
parking) + the --variant cost measurement. Gate green (ruff/mypy + 51 tests;
node --check the plugins).
This commit is contained in:
@@ -13,16 +13,15 @@ FROM roboco-agent-base
|
||||
USER root
|
||||
|
||||
# opencode — the OpenAI-protocol agent runtime. grok-build-0.1 runs on opencode's
|
||||
# BUILT-IN xai provider (no custom provider npm — that breaks model resolution),
|
||||
# so only opencode-ai is installed; it resolves the provider SDK at runtime.
|
||||
# BUILT-IN xai provider (no custom provider block / npm needed), so only
|
||||
# opencode-ai is installed; it resolves the provider SDK at runtime.
|
||||
RUN npm install -g opencode-ai \
|
||||
&& npm cache clean --force \
|
||||
&& rm -rf /root/.npm /tmp/*
|
||||
|
||||
# opencode plugins, baked into the AUTO-DISCOVERY dir (~/.config/opencode/plugin/).
|
||||
# opencode 1.17.8 does NOT register a plugin's hooks/tools from a config
|
||||
# `plugin:`-array absolute path — only from this directory (verified live). Each
|
||||
# plugin uses a NAMED export.
|
||||
# opencode plugins, baked into the AUTO-DISCOVERY dir (~/.config/opencode/plugin/)
|
||||
# rather than referenced by a config `plugin:` path — the dir is the simplest
|
||||
# registration route. Each plugin uses a NAMED export (opencode's convention).
|
||||
# secret-scrub — bash-guard parity (PAT/credential deny on tool.execute.before)
|
||||
# budget-feed — POSTs budget/loop/terminal counters to the in-container SDK
|
||||
# server (tool.execute.{before,after}); the entrypoint starts
|
||||
|
||||
@@ -77,9 +77,9 @@ function bareVerb(tool) {
|
||||
return tool;
|
||||
}
|
||||
|
||||
// Named export + loaded from the plugin auto-discovery dir
|
||||
// (~/.config/opencode/plugin/) — opencode 1.17.8 ignores config `plugin:`-array
|
||||
// absolute paths for hook/tool registration (verified live).
|
||||
// Named export (opencode's plugin convention) + baked into the plugin
|
||||
// auto-discovery dir (~/.config/opencode/plugin/) at image build — the simplest
|
||||
// registration route (no config `plugin:` path needed).
|
||||
export const RobocoBudgetFeed = async () => {
|
||||
return {
|
||||
"tool.execute.before": async (input) => {
|
||||
|
||||
+45
-23
@@ -1,29 +1,28 @@
|
||||
// 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.
|
||||
// The model calls propose_draft once the task spec is ready; this delivers the
|
||||
// draft to the panel's 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.
|
||||
// WHY IT POSTS DIRECTLY (not via the driver): opencode's synchronous serve reply
|
||||
// (POST /session/:id/message) returns only the final assistant text + step
|
||||
// markers — NOT the tool-CALL parts. So OpencodeServeSession cannot intercept
|
||||
// this call to emit a `draft` chunk (verified live: a propose_draft call comes
|
||||
// back as parts=[step-start, text, step-finish], no tool part). Instead the tool
|
||||
// POSTs the draft straight to the prompter-live relay — the same
|
||||
// /api/prompter/live/{session}/events endpoint the driver's relay sink uses — so
|
||||
// the panel renders the card regardless. (The Claude intake path differs: the
|
||||
// Claude SDK DOES expose the tool-use block, so its driver intercepts it.)
|
||||
//
|
||||
// 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.
|
||||
// Loaded from the plugin auto-discovery dir (~/.config/opencode/plugin/), baked
|
||||
// into the grok-prompter image only (the one-shot delivery roles never draft).
|
||||
// The container provides ROBOCO_API_URL + ROBOCO_PROMPTER_SESSION_ID.
|
||||
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
// Named export + loaded from the plugin auto-discovery dir
|
||||
// (~/.config/opencode/plugin/) — opencode 1.17.8 only registers Hooks.tool from
|
||||
// directory auto-discovery, not a config `plugin:`-array absolute path
|
||||
// (verified live).
|
||||
const API_BASE = (
|
||||
process.env.ROBOCO_API_URL || "http://roboco-orchestrator:8000"
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
export const RobocoIntakeTools = async () => ({
|
||||
tool: {
|
||||
propose_draft: tool({
|
||||
@@ -38,10 +37,33 @@ export const RobocoIntakeTools = async () => ({
|
||||
.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.";
|
||||
async execute(args) {
|
||||
const session = process.env.ROBOCO_PROMPTER_SESSION_ID || "";
|
||||
if (!session) {
|
||||
return "No live session id (ROBOCO_PROMPTER_SESSION_ID) — cannot surface the draft.";
|
||||
}
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/prompter/live/${encodeURIComponent(session)}/events`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
kind: "draft",
|
||||
text: "",
|
||||
tool: "propose_draft",
|
||||
data: args.draft || {},
|
||||
}),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
return `Draft relay returned HTTP ${res.status}; the human may not see the card.`;
|
||||
}
|
||||
} catch (e) {
|
||||
return "Could not submit the draft to the panel: " + String(e);
|
||||
}
|
||||
return "Draft submitted — the human can review it in the panel.";
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -164,10 +164,10 @@ function denyBash(command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Named export + loaded from opencode's plugin auto-discovery dir
|
||||
// (~/.config/opencode/plugin/). opencode 1.17.8 does NOT register a plugin's
|
||||
// hooks/tools when it's listed by absolute path in the config `plugin:` array —
|
||||
// only directory auto-discovery works (verified live against grok-build-0.1).
|
||||
// Named export (opencode's plugin convention) + loaded from opencode's plugin
|
||||
// auto-discovery dir (~/.config/opencode/plugin/), where it's baked at image
|
||||
// build — the simplest registration route (no config `plugin:` path needed).
|
||||
// Hook firing verified live against grok-build-0.1.
|
||||
export const RobocoSecretScrub = async () => {
|
||||
return {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
|
||||
@@ -66,11 +66,10 @@ async function callBackend(method, path, body) {
|
||||
|
||||
const asText = (data) => JSON.stringify(data);
|
||||
|
||||
// Named export + loaded from the plugin auto-discovery dir
|
||||
// (~/.config/opencode/plugin/) — opencode 1.17.8 does NOT register tools from a
|
||||
// config `plugin:`-array absolute path; only directory auto-discovery + a named
|
||||
// export registers Hooks.tool (verified live: the model called the tool and the
|
||||
// backend received the request).
|
||||
// Named export (opencode's plugin convention) + baked into the plugin
|
||||
// auto-discovery dir (~/.config/opencode/plugin/) at image build. Verified live
|
||||
// against grok-build-0.1: the model called read_company_state + submit_directive
|
||||
// and the backend received both requests with the X-Agent-Token.
|
||||
export const RobocoSecretaryTools = async () => ({
|
||||
tool: {
|
||||
read_company_state: tool({
|
||||
|
||||
Reference in New Issue
Block a user