@zeroroot-ai/zerocool 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,149 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { buildComponentManifest, enrollComponent, enrollmentSupported, validateComponentSpec, } from "@zeroroot-ai/sdk";
3
- import { mkdir, writeFile } from "node:fs/promises";
4
- import { dirname, resolve } from "node:path";
5
- /**
6
- * Componentize produced artifacts into the tenant fleet (zerocool-plugins#11).
7
- *
8
- * The factory payoff: an artifact the agent built becomes a registered Gibson
9
- * component. The contract is language-neutral — `RegisterComponent` describes a
10
- * component by kind, name, version and contract fields, never by the language
11
- * it was written in.
12
- *
13
- * Two tools, because the work splits at a real boundary:
14
- *
15
- * - `gibson_componentize` writes the manifest to disk without touching the
16
- * platform. It is the step that can always run, and the artefact it produces
17
- * is what a build pipeline consumes.
18
- * - `gibson_enroll_component` registers the manifest with the daemon.
19
- *
20
- * WHAT THIS DOES NOT DO. Image build and publish is a build-system concern, not
21
- * an RPC; the OCI reference is an input here, carried in `metadata.image`. And
22
- * enrollment currently registers under the CALLING agent's identity — the
23
- * policy-bounded RPC that would give a produced component its own short-TTL
24
- * credential is gibson#1185 and does not exist yet. The tool says so in its
25
- * output rather than implying the artifact is independently dispatchable.
26
- */
27
- const KINDS = ["agent", "tool", "plugin"];
28
- /** Shared argument shape for both tools. */
29
- function specArgs() {
30
- const z = tool.schema;
31
- return {
32
- kind: z.enum(KINDS).describe("Component kind."),
33
- name: z.string().describe("Component name, as it will appear in the tenant registry."),
34
- version: z.string().describe('Semantic version of the artifact, e.g. "0.1.0".'),
35
- image: z
36
- .string()
37
- .optional()
38
- .describe("OCI image reference of the built artifact. Required before it can be dispatched."),
39
- language: z.string().optional().describe("Language the artifact is written in, for the catalog."),
40
- capabilities: z
41
- .array(z.string())
42
- .optional()
43
- .describe('Agent capabilities. Required when kind is "agent".'),
44
- methods: z.array(z.string()).optional().describe('Plugin method names. Required when kind is "plugin".'),
45
- input_message_type: z
46
- .string()
47
- .optional()
48
- .describe('Fully-qualified proto input type. Required when kind is "tool".'),
49
- output_message_type: z
50
- .string()
51
- .optional()
52
- .describe('Fully-qualified proto output type. Required when kind is "tool".'),
53
- config_schema_json: z.string().optional().describe("JSON Schema for a plugin's configuration."),
54
- };
55
- }
56
- /** Build a ComponentSpec from tool arguments. */
57
- function toSpec(args) {
58
- const metadata = {};
59
- if (args.image)
60
- metadata.image = args.image;
61
- if (args.language)
62
- metadata.language = args.language;
63
- return {
64
- kind: args.kind,
65
- name: args.name,
66
- version: args.version,
67
- metadata,
68
- ...(args.capabilities ? { capabilities: args.capabilities } : {}),
69
- ...(args.methods ? { methods: args.methods } : {}),
70
- ...(args.input_message_type ? { inputMessageType: args.input_message_type } : {}),
71
- ...(args.output_message_type ? { outputMessageType: args.output_message_type } : {}),
72
- ...(args.config_schema_json ? { configSchemaJson: args.config_schema_json } : {}),
73
- };
74
- }
75
- /**
76
- * Write a validated component manifest to disk. Available with or without a
77
- * Gibson session — the manifest is the artifact a build pipeline needs, and
78
- * producing it must not require a platform connection.
79
- */
80
- export function componentizeTool() {
81
- const z = tool.schema;
82
- return tool({
83
- description: "Turn a built artifact into a Gibson component manifest and write it to disk. " +
84
- "Use this after building a tool or agent you want to add to the fleet. Validates " +
85
- "the artifact against Gibson's component contract; does not build or push an image.",
86
- args: {
87
- ...specArgs(),
88
- path: z
89
- .string()
90
- .optional()
91
- .describe('Where to write the manifest. Defaults to "gibson-component.json".'),
92
- },
93
- async execute(args, ctx) {
94
- const spec = toSpec(args);
95
- const problems = validateComponentSpec(spec);
96
- if (problems.length > 0) {
97
- return {
98
- title: "invalid component spec",
99
- output: `This artifact does not satisfy Gibson's ${spec.kind} contract:\n${problems.map((p) => `- ${p}`).join("\n")}`,
100
- };
101
- }
102
- const manifest = buildComponentManifest(spec);
103
- const target = resolve(ctx.directory, args.path ?? "gibson-component.json");
104
- await mkdir(dirname(target), { recursive: true });
105
- await writeFile(target, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
106
- const missingImage = spec.metadata?.image
107
- ? ""
108
- : "\n\nNo image reference was given. Build and push an image, then re-run with " +
109
- "image=<oci-ref> before enrolling — a component without an image cannot be dispatched.";
110
- return {
111
- title: `${spec.kind} manifest: ${spec.name}`,
112
- output: `Wrote a valid ${spec.kind} manifest for ${spec.name}@${spec.version} to ${target}.${missingImage}`,
113
- metadata: { path: target, kind: spec.kind, name: spec.name },
114
- };
115
- },
116
- });
117
- }
118
- /** Register a produced artifact with the tenant registry. */
119
- export function enrollComponentTool(session) {
120
- return tool({
121
- description: "Register a produced artifact with Gibson so it joins the tenant fleet. Run " +
122
- "gibson_componentize first to check the artifact against the component contract.",
123
- args: specArgs(),
124
- async execute(args) {
125
- const spec = toSpec(args);
126
- const problems = validateComponentSpec(spec);
127
- if (problems.length > 0) {
128
- return {
129
- title: "invalid component spec",
130
- output: `This artifact does not satisfy Gibson's ${spec.kind} contract:\n${problems.map((p) => `- ${p}`).join("\n")}`,
131
- };
132
- }
133
- const result = await enrollComponent(session.clients.component, spec);
134
- const { reason } = enrollmentSupported();
135
- return {
136
- title: `enrolled ${spec.name}`,
137
- output: `Registered ${spec.kind} ${spec.name}@${spec.version} as instance ${result.instanceId}.\n\n` +
138
- `Note: ${reason}. It is visible in the tenant registry, but it will drop out ` +
139
- `when heartbeats stop (every ${Math.round(result.heartbeatIntervalMs / 1000)}s) ` +
140
- `unless the artifact itself runs and checks in.`,
141
- metadata: {
142
- instance_id: result.instanceId,
143
- has_own_identity: result.hasOwnIdentity,
144
- kind: spec.kind,
145
- },
146
- };
147
- },
148
- });
149
- }
@@ -1,11 +0,0 @@
1
- import type { ToolDefinition } from "@opencode-ai/plugin";
2
- import { type GibsonSession } from "@zeroroot-ai/sdk";
3
- export declare function listAgentsTool(session: GibsonSession): ToolDefinition;
4
- export declare function delegateTool(session: GibsonSession): ToolDefinition;
5
- export declare function createMissionTool(session: GibsonSession): ToolDefinition;
6
- export declare function runMissionTool(session: GibsonSession): ToolDefinition;
7
- export declare function missionStatusTool(session: GibsonSession): ToolDefinition;
8
- export declare function missionResultsTool(session: GibsonSession): ToolDefinition;
9
- export declare function cancelMissionTool(session: GibsonSession): ToolDefinition;
10
- /** Every delegation and mission tool, keyed for the opencode `tool` hook. */
11
- export declare function delegationTools(session: GibsonSession): Record<string, ToolDefinition>;
package/dist/delegate.js DELETED
@@ -1,216 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { cancelMission, createMission, delegateToAgent, getMissionResults, getMissionStatus, isSeamUnavailable, listAgents, newTask, runMission, waitMission, } from "@zeroroot-ai/sdk";
3
- /**
4
- * Delegation and missions — the factory's dispatch and recursion
5
- * (zerocool-plugins#10).
6
- *
7
- * Rules-of-engagement, authz and budget denials come back as daemon errors.
8
- * They are surfaced verbatim to the model rather than retried: a denial is a
9
- * policy decision, and an agent that retries a denied delegation is just
10
- * burning budget against a wall.
11
- *
12
- * Every RPC here needs the daemon's `agentDelegator` / `missionMgr` seams,
13
- * which are not wired (gibson#1186). Each tool reports that state plainly
14
- * instead of surfacing a bare "unimplemented" to the model.
15
- */
16
- const UNWIRED = "This daemon has no agent delegation or mission management wired (gibson#1186). " +
17
- "Complete the work directly instead.";
18
- export function listAgentsTool(session) {
19
- return tool({
20
- description: "List the Gibson agents in this tenant that work can be delegated to, with " +
21
- "their capabilities and the target types they handle.",
22
- args: {},
23
- async execute() {
24
- const { agents, unavailable } = await listAgents(session.clients.component);
25
- if (unavailable)
26
- return { title: "delegation unavailable", output: UNWIRED };
27
- if (agents.length === 0) {
28
- return { title: "no agents", output: "No agents are registered for this tenant." };
29
- }
30
- const lines = agents.map((a) => {
31
- const caps = a.capabilities.length > 0 ? ` — capabilities: ${a.capabilities.join(", ")}` : "";
32
- const targets = a.targetTypes.length > 0 ? ` — targets: ${a.targetTypes.join(", ")}` : "";
33
- return `- ${a.name} (${a.version}) ${a.description}${caps}${targets}`;
34
- });
35
- return {
36
- title: `${agents.length} agent${agents.length === 1 ? "" : "s"}`,
37
- output: lines.join("\n"),
38
- metadata: { agent_count: agents.length },
39
- };
40
- },
41
- });
42
- }
43
- export function delegateTool(session) {
44
- const z = tool.schema;
45
- return tool({
46
- description: "Delegate a sub-task to another Gibson agent and wait for its result. Use " +
47
- "gibson_list_agents first to pick an agent whose capabilities match the task.",
48
- args: {
49
- agent: z.string().describe("Name of the agent to delegate to."),
50
- goal: z.string().describe("What the delegate should accomplish."),
51
- context: z
52
- .record(z.string(), z.any())
53
- .optional()
54
- .describe("Target details, prior findings, or other context the delegate needs."),
55
- max_turns: z.number().optional().describe("Cap on the delegate's LLM turns."),
56
- allowed_tools: z.array(z.string()).optional().describe("Restrict the delegate to these tools."),
57
- },
58
- async execute(args) {
59
- const constraints = {
60
- ...(args.max_turns ? { MaxTurns: args.max_turns } : {}),
61
- ...(args.allowed_tools ? { AllowedTools: args.allowed_tools } : {}),
62
- };
63
- const task = newTask(args.goal, {
64
- ...(args.context ? { Context: args.context } : {}),
65
- ...(Object.keys(constraints).length > 0 ? { Constraints: constraints } : {}),
66
- });
67
- try {
68
- const result = await delegateToAgent(session.clients.component, args.agent, task);
69
- const output = typeof result.Output === "string" ? result.Output : JSON.stringify(result.Output, null, 2);
70
- const findings = result.Findings && result.Findings.length > 0
71
- ? `\n\nFindings recorded: ${result.Findings.join(", ")}`
72
- : "";
73
- const failure = result.error?.message ? `\n\nError: ${result.error.message}` : "";
74
- return {
75
- title: `${args.agent}: ${result.Status}`,
76
- output: `${output ?? "(no output)"}${findings}${failure}`,
77
- metadata: { status: result.Status, finding_ids: result.Findings ?? [] },
78
- };
79
- }
80
- catch (e) {
81
- if (isSeamUnavailable(e))
82
- return { title: "delegation unavailable", output: UNWIRED };
83
- // Authz / RoE / budget denials land here. Show the daemon's reason.
84
- return {
85
- title: `${args.agent}: denied`,
86
- output: `Delegation to ${args.agent} was refused: ${e.message}`,
87
- };
88
- }
89
- },
90
- });
91
- }
92
- export function createMissionTool(session) {
93
- const z = tool.schema;
94
- return tool({
95
- description: "Create a Gibson mission from a mission definition, bound to a target. The " +
96
- "mission is not started — call gibson_run_mission to queue it.",
97
- args: {
98
- definition: z.record(z.string(), z.any()).describe("Mission definition object."),
99
- target_id: z.string().describe("Identifier of the target the mission runs against."),
100
- opts: z.record(z.string(), z.any()).optional().describe("Optional mission creation options."),
101
- },
102
- async execute(args) {
103
- try {
104
- const info = await createMission(session.clients.component, args.definition, args.target_id, args.opts);
105
- const id = (info.id ?? info.mission_id ?? info.missionId);
106
- return {
107
- title: id ? `mission ${id}` : "mission created",
108
- output: JSON.stringify(info, null, 2),
109
- ...(id ? { metadata: { mission_id: id } } : {}),
110
- };
111
- }
112
- catch (e) {
113
- if (isSeamUnavailable(e))
114
- return { title: "missions unavailable", output: UNWIRED };
115
- throw e;
116
- }
117
- },
118
- });
119
- }
120
- export function runMissionTool(session) {
121
- const z = tool.schema;
122
- return tool({
123
- description: "Queue a created Gibson mission for execution. Returns as soon as it is queued.",
124
- args: { mission_id: z.string().describe("Mission to run.") },
125
- async execute(args) {
126
- try {
127
- await runMission(session.clients.component, args.mission_id);
128
- return { title: "queued", output: `Mission ${args.mission_id} is queued for execution.` };
129
- }
130
- catch (e) {
131
- if (isSeamUnavailable(e))
132
- return { title: "missions unavailable", output: UNWIRED };
133
- throw e;
134
- }
135
- },
136
- });
137
- }
138
- export function missionStatusTool(session) {
139
- const z = tool.schema;
140
- return tool({
141
- description: "Check a Gibson mission's status, or block until it finishes. Use wait=true " +
142
- "only when you intend to stop and wait for the result.",
143
- args: {
144
- mission_id: z.string().describe("Mission to inspect."),
145
- wait: z.boolean().optional().describe("Block until the mission reaches a terminal state."),
146
- timeout_ms: z
147
- .number()
148
- .optional()
149
- .describe("How long to block when wait is true. Defaults to 5 minutes."),
150
- },
151
- async execute(args) {
152
- try {
153
- const state = args.wait
154
- ? await waitMission(session.clients.component, args.mission_id, args.timeout_ms ?? 300_000)
155
- : await getMissionStatus(session.clients.component, args.mission_id);
156
- return {
157
- title: `mission ${args.mission_id}`,
158
- output: JSON.stringify(state, null, 2),
159
- };
160
- }
161
- catch (e) {
162
- if (isSeamUnavailable(e))
163
- return { title: "missions unavailable", output: UNWIRED };
164
- throw e;
165
- }
166
- },
167
- });
168
- }
169
- export function missionResultsTool(session) {
170
- const z = tool.schema;
171
- return tool({
172
- description: "Read the final results of a completed Gibson mission.",
173
- args: { mission_id: z.string().describe("Mission whose results to read.") },
174
- async execute(args) {
175
- try {
176
- const results = await getMissionResults(session.clients.component, args.mission_id);
177
- return { title: `results ${args.mission_id}`, output: JSON.stringify(results, null, 2) };
178
- }
179
- catch (e) {
180
- if (isSeamUnavailable(e))
181
- return { title: "missions unavailable", output: UNWIRED };
182
- throw e;
183
- }
184
- },
185
- });
186
- }
187
- export function cancelMissionTool(session) {
188
- const z = tool.schema;
189
- return tool({
190
- description: "Request cancellation of a running Gibson mission.",
191
- args: { mission_id: z.string().describe("Mission to cancel.") },
192
- async execute(args) {
193
- try {
194
- await cancelMission(session.clients.component, args.mission_id);
195
- return { title: "cancelled", output: `Cancellation requested for mission ${args.mission_id}.` };
196
- }
197
- catch (e) {
198
- if (isSeamUnavailable(e))
199
- return { title: "missions unavailable", output: UNWIRED };
200
- throw e;
201
- }
202
- },
203
- });
204
- }
205
- /** Every delegation and mission tool, keyed for the opencode `tool` hook. */
206
- export function delegationTools(session) {
207
- return {
208
- gibson_list_agents: listAgentsTool(session),
209
- gibson_delegate: delegateTool(session),
210
- gibson_create_mission: createMissionTool(session),
211
- gibson_run_mission: runMissionTool(session),
212
- gibson_mission_status: missionStatusTool(session),
213
- gibson_mission_results: missionResultsTool(session),
214
- gibson_cancel_mission: cancelMissionTool(session),
215
- };
216
- }
@@ -1,45 +0,0 @@
1
- import type { ToolDefinition } from "@opencode-ai/plugin";
2
- import { type GibsonSession, type GibsonTool } from "@zeroroot-ai/sdk";
3
- /**
4
- * Gibson tools and plugins as opencode tools (zerocool-plugins#9).
5
- *
6
- * Discovered tools are registered **individually** — one opencode tool per
7
- * Gibson tool, named `gibson_<tool>` — so the model sees them in its tool list
8
- * with their own descriptions, rather than having to know that a generic
9
- * dispatcher exists and what to pass it.
10
- *
11
- * ON SCHEMAS — the catalog carries no JSON Schema. `ToolDescriptorProto` only
12
- * names the proto message types of a tool's input and output
13
- * (`gibson.tool.v1.ScanRequest`), and there is no RPC that resolves those to a
14
- * schema. So a per-tool wrapper takes a single free-form `input` object and
15
- * names the expected message type in its description. Inventing a Zod schema
16
- * from a message type name would be a guess, and a wrong guess would reject
17
- * valid input before it ever reached the tool.
18
- *
19
- * A generic `gibson_call_tool` is always registered as well: discovery is
20
- * `Unimplemented` on the current daemon (gibson#1186), and without it there
21
- * would be no way to reach a tool whose name the operator already knows.
22
- */
23
- /** Sanitise a Gibson tool name into an opencode tool key. */
24
- export declare function toolKey(name: string): string;
25
- /** Wrap one discovered Gibson tool as an opencode tool. */
26
- export declare function wrapGibsonTool(session: GibsonSession, t: GibsonTool): ToolDefinition;
27
- /** The always-present dispatcher for tools the catalog did not surface. */
28
- export declare function callToolTool(session: GibsonSession): ToolDefinition;
29
- /** List what Gibson exposes to this tenant — tools and enabled plugins. */
30
- export declare function listToolsTool(session: GibsonSession): ToolDefinition;
31
- /** Invoke a method on an enabled Gibson plugin. */
32
- export declare function queryPluginTool(session: GibsonSession): ToolDefinition;
33
- /**
34
- * Discover the tenant's tools and build one opencode tool for each, plus the
35
- * always-present dispatcher, catalog and plugin tools.
36
- *
37
- * Discovery runs once at plugin load. opencode reads the `tool` hook as a
38
- * static object, so a tool registered later in the session would not be visible
39
- * to the model anyway.
40
- */
41
- export declare function buildGibsonTools(session: GibsonSession): Promise<{
42
- tools: Record<string, ToolDefinition>;
43
- discovered: number;
44
- note?: string;
45
- }>;
@@ -1,175 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { callGibsonTool, listGibsonPlugins, listGibsonTools, queryGibsonPlugin, } from "@zeroroot-ai/sdk";
3
- /**
4
- * Gibson tools and plugins as opencode tools (zerocool-plugins#9).
5
- *
6
- * Discovered tools are registered **individually** — one opencode tool per
7
- * Gibson tool, named `gibson_<tool>` — so the model sees them in its tool list
8
- * with their own descriptions, rather than having to know that a generic
9
- * dispatcher exists and what to pass it.
10
- *
11
- * ON SCHEMAS — the catalog carries no JSON Schema. `ToolDescriptorProto` only
12
- * names the proto message types of a tool's input and output
13
- * (`gibson.tool.v1.ScanRequest`), and there is no RPC that resolves those to a
14
- * schema. So a per-tool wrapper takes a single free-form `input` object and
15
- * names the expected message type in its description. Inventing a Zod schema
16
- * from a message type name would be a guess, and a wrong guess would reject
17
- * valid input before it ever reached the tool.
18
- *
19
- * A generic `gibson_call_tool` is always registered as well: discovery is
20
- * `Unimplemented` on the current daemon (gibson#1186), and without it there
21
- * would be no way to reach a tool whose name the operator already knows.
22
- */
23
- /** Sanitise a Gibson tool name into an opencode tool key. */
24
- export function toolKey(name) {
25
- return `gibson_${name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
26
- }
27
- /** Wrap one discovered Gibson tool as an opencode tool. */
28
- export function wrapGibsonTool(session, t) {
29
- const z = tool.schema;
30
- const schemaNote = t.inputMessageType
31
- ? ` Input must match the Gibson message type ${t.inputMessageType}.`
32
- : "";
33
- return tool({
34
- description: `${t.description || `Gibson tool "${t.name}"`}${schemaNote} Runs through the Gibson harness (authorized and metered).`,
35
- args: {
36
- input: z
37
- .record(z.string(), z.any())
38
- .describe(`Input object for the ${t.name} tool${t.inputMessageType ? ` (${t.inputMessageType})` : ""}.`),
39
- timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
40
- },
41
- async execute(args) {
42
- const result = await callGibsonTool(session.clients.component, {
43
- name: t.name,
44
- input: args.input,
45
- ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
46
- });
47
- return formatToolResult(t.name, result);
48
- },
49
- });
50
- }
51
- /** The always-present dispatcher for tools the catalog did not surface. */
52
- export function callToolTool(session) {
53
- const z = tool.schema;
54
- return tool({
55
- description: "Invoke a Gibson tool by name through the harness. Use this when you know a " +
56
- "tool's name but it is not in your tool list — Gibson tool discovery is not " +
57
- "available on every daemon.",
58
- args: {
59
- name: z.string().describe("Registered Gibson tool name."),
60
- input: z.record(z.string(), z.any()).describe("Input object matching the tool's input schema."),
61
- timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
62
- },
63
- async execute(args) {
64
- const result = await callGibsonTool(session.clients.component, {
65
- name: args.name,
66
- input: args.input,
67
- ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
68
- });
69
- return formatToolResult(args.name, result);
70
- },
71
- });
72
- }
73
- /** List what Gibson exposes to this tenant — tools and enabled plugins. */
74
- export function listToolsTool(session) {
75
- return tool({
76
- description: "List the Gibson tools and plugins available to this tenant, with the input " +
77
- "message type each tool expects.",
78
- args: {},
79
- async execute() {
80
- const [discovery, plugins] = await Promise.all([
81
- listGibsonTools(session.clients.component),
82
- listGibsonPlugins(session.clients.component).catch(() => []),
83
- ]);
84
- const lines = [];
85
- if (discovery.unavailable) {
86
- lines.push(`Tool discovery is unavailable: ${discovery.unavailable}.`);
87
- }
88
- else if (discovery.tools.length === 0) {
89
- lines.push("No Gibson tools are registered for this tenant.");
90
- }
91
- else {
92
- lines.push("Gibson tools:");
93
- for (const t of discovery.tools) {
94
- lines.push(`- ${t.name} (${t.version}) — ${t.description || "no description"}`);
95
- if (t.inputMessageType)
96
- lines.push(` input: ${t.inputMessageType}`);
97
- }
98
- }
99
- const enabled = plugins.filter((p) => p.enabled);
100
- if (enabled.length > 0) {
101
- lines.push("", "Enabled plugins:");
102
- for (const p of enabled) {
103
- lines.push(`- ${p.name} (${p.version}) — methods: ${p.methods.join(", ") || "none"}`);
104
- }
105
- }
106
- return {
107
- title: `${discovery.tools.length} tools, ${enabled.length} plugins`,
108
- output: lines.join("\n"),
109
- metadata: { tool_count: discovery.tools.length, plugin_count: enabled.length },
110
- };
111
- },
112
- });
113
- }
114
- /** Invoke a method on an enabled Gibson plugin. */
115
- export function queryPluginTool(session) {
116
- const z = tool.schema;
117
- return tool({
118
- description: "Call a method on an enabled Gibson plugin through the harness. Use " +
119
- "gibson_list_tools first to see which plugins and methods are available.",
120
- args: {
121
- plugin: z.string().describe("Registered plugin name."),
122
- method: z.string().describe("Method to invoke on the plugin."),
123
- params: z.record(z.string(), z.any()).optional().describe("Method parameters."),
124
- timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
125
- },
126
- async execute(args) {
127
- const result = await queryGibsonPlugin(session.clients.component, {
128
- plugin: args.plugin,
129
- method: args.method,
130
- ...(args.params ? { params: args.params } : {}),
131
- ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
132
- });
133
- return formatToolResult(`${args.plugin}.${args.method}`, result);
134
- },
135
- });
136
- }
137
- /**
138
- * Discover the tenant's tools and build one opencode tool for each, plus the
139
- * always-present dispatcher, catalog and plugin tools.
140
- *
141
- * Discovery runs once at plugin load. opencode reads the `tool` hook as a
142
- * static object, so a tool registered later in the session would not be visible
143
- * to the model anyway.
144
- */
145
- export async function buildGibsonTools(session) {
146
- const tools = {
147
- gibson_call_tool: callToolTool(session),
148
- gibson_list_tools: listToolsTool(session),
149
- gibson_query_plugin: queryPluginTool(session),
150
- };
151
- const discovery = await listGibsonTools(session.clients.component);
152
- for (const t of discovery.tools) {
153
- // Never let a discovered tool shadow the built-ins.
154
- const key = toolKey(t.name);
155
- if (!(key in tools))
156
- tools[key] = wrapGibsonTool(session, t);
157
- }
158
- return {
159
- tools,
160
- discovered: discovery.tools.length,
161
- ...(discovery.unavailable ? { note: discovery.unavailable } : {}),
162
- };
163
- }
164
- /** Render a harness tool result, keeping an in-band tool error visible. */
165
- function formatToolResult(name, result) {
166
- if (result.error) {
167
- return {
168
- title: `${name} failed`,
169
- output: `Gibson tool ${name} failed [${result.error.code}]: ${result.error.message}${result.error.retryable ? " (retryable)" : ""}`,
170
- metadata: { error_code: result.error.code, retryable: result.error.retryable },
171
- };
172
- }
173
- const text = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
174
- return { title: name, output: text ?? "(no output)" };
175
- }