@dougbots/avenor-opencode 0.14.0 → 0.16.0

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.
Files changed (3) hide show
  1. package/README.md +63 -10
  2. package/dist/index.js +226 -43
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -33,7 +33,7 @@ Add `@dougbots/avenor-opencode` to your OpenCode configuration. If you use a glo
33
33
 
34
34
  For a project-level config, add the same entry to `.opencode.json` in your repo root.
35
35
 
36
- Once registered, the plugin exposes eight tools that your OpenCode agent can call directly — no extra configuration required.
36
+ Once registered, the plugin exposes these tools that your OpenCode agent can call directly — no extra configuration required: `avenor_spawn`, `avenor_status`, `avenor_result`, `avenor_inspect`, `avenor_answer_permission`, `avenor_message`, `avenor_peers`, `avenor_events`, `avenor_kill`, `avenor_shutdown`, `avenor_workflow_cancel`, `avenor_workflow_status`, `avenor_workflow_wait`, `avenor_workflow_inspect`, `avenor_workflow_events`, `avenor_workflow_complete`, `avenor_workflow_gate`, and `avenor_workflow_controller_status`.
37
37
 
38
38
  ## Behaviour
39
39
 
@@ -42,7 +42,8 @@ When a run is dispatched with `avenor_spawn`, the plugin:
42
42
  1. **Blocks by default** — the tool call stays open, updating its title and metadata as the sub-agent runs, exactly like OpenCode's own sub-agent tool calls. The tool returns when the run reaches a terminal state.
43
43
  2. **Re-prompts on completion** — when a `wait=false` (fire-and-forget) run finishes, the plugin automatically injects a completion message into the orchestrating session so the LLM picks up without manual polling.
44
44
  3. **Routes permissions** — when a sub-agent running under the `opencode-http` backend requests a permission, the plugin injects a re-prompt into the orchestrating session. The LLM can call `avenor_answer_permission` to respond; the normal permission dialog is also shown as a fallback.
45
- 4. **Formats results** — OpenCode returns concise, bounded prose in its shared `output` for `avenor_status`, `avenor_result`, `avenor_answer_permission`, `avenor_follow_up`, `avenor_events`, `avenor_inspect`, and `avenor_shutdown`. Structured metadata stays in host/session state.
45
+ 4. **Formats results** — OpenCode returns concise, bounded prose in its shared `output` for `avenor_status`, `avenor_result`, `avenor_answer_permission`, `avenor_message`, `avenor_peers`, `avenor_events`, `avenor_inspect`, and `avenor_shutdown`. Structured metadata stays in host/session state.
46
+ 5. **Publishes its own finished turn** — a settled turn from the session that first ran a host messaging tool is recorded through `session_last_set`, so a peer's `avenor_peers` `action: "last"` can read it.
46
47
 
47
48
  MCP clients render their own results.
48
49
 
@@ -133,16 +134,34 @@ Answer a pending permission request. The `option_id` comes from the `pending_per
133
134
  | `request_id` | no | Request ID (auto-discovered if omitted) |
134
135
  | `supervisor_id` | no | Reuse an existing supervisor by socket path |
135
136
 
136
- ### `avenor_follow_up`
137
+ ### `avenor_message`
137
138
 
138
- Resume a completed run with a follow-up message. Useful for iterating on results or asking the sub-agent to refine its output.
139
+ Send a message to another run, withdraw a pending ask, resume a completed run, or deliver a control reply. Every action is explicit and every result states whether a failed ask can still be withdrawn.
139
140
 
140
- | Argument | Required | Description |
141
+ | `action` | Required fields | What it does |
141
142
  |---|---|---|
142
- | `run_id` | yes | Completed run ID to resume |
143
- | `message` | yes | Follow-up message text |
144
- | `label` | no | Override label (defaults to `<original>-followup`) |
145
- | `supervisor_id` | no | Reuse an existing supervisor by socket path |
143
+ | `tell` | `target`, `message` | Deliver a message without creating an ask edge |
144
+ | `ask` | `target`, `message`, optional `timeout_ms` | Deliver a message and wait for a reply |
145
+ | `reply` | `reply_to`, `message` | Answer an inbound ask. This session receives no inbound delivery, so the result is always `edge-absent` and nothing is sent |
146
+ | `resume` | `target`, `message`, optional `label`, `idempotency_key` | Resume a completed run from its persisted session |
147
+ | `withdraw` | `message_id` | Withdraw a pending ask |
148
+ | `control_reply` | `target`, optional `payload` | Deliver a control reply where the provider supports it |
149
+
150
+ `target` is a run id, a peer label, or `"self"`. `timeout_ms` must be an integer no greater than the shared ask cap. This tool does not take `supervisor_id`; it is bound to the default supervisor.
151
+
152
+ The tool's structured metadata is the shared contract object in snake_case (`action`, `outcome`, `text`, and `message_id`, `run_id`, `label`, `reply`, `sentinel_file`, `on_event` when the outcome carries them), the same fields the MCP hosts return.
153
+
154
+ ### `avenor_peers`
155
+
156
+ List the visible agent sessions, rename a run, or read a run's last agent message.
157
+
158
+ | `action` | Fields | What it does |
159
+ |---|---|---|
160
+ | `list` (default) | — | List the visible peers |
161
+ | `rename` | `name`, optional `session_id` | Change a display name, self by default |
162
+ | `last` | `target` | Read the target's last agent message with its stop reason |
163
+
164
+ `last` is read-only and returns the shared empty result when the target has no completed turn yet. `target` accepts a run id, a label, or `"self"`. The structured metadata is `{peers}`, `{renamed}` or `{last}` in snake_case, the same object the MCP hosts return.
146
165
 
147
166
  ### `avenor_events`
148
167
 
@@ -155,6 +174,40 @@ Read events from a run, optionally filtered by type. Returns the last N events.
155
174
  | `limit` | no | Max events to return (default 50) |
156
175
  | `supervisor_id` | no | Reuse an existing supervisor by socket path |
157
176
 
177
+ ### `avenor_kill`
178
+
179
+ Terminate a run without shutting down the supervisor. Other runs and the supervisor itself keep running.
180
+
181
+ | Argument | Required | Description |
182
+ |---|---|---|
183
+ | `run_id` | yes | Run ID or label of the run to terminate |
184
+ | `supervisor_id` | no | Supervisor ID for multi-supervisor mode |
185
+
186
+ ### `avenor_workflow_cancel`
187
+
188
+ Cancel a workflow instance. Both the actor and the reason are required; the manager rejects a missing actor before it looks at the reason.
189
+
190
+ | Argument | Required | Description |
191
+ |---|---|---|
192
+ | `workflow_id` | yes | Workflow ID |
193
+ | `actor` | yes | Who is cancelling |
194
+ | `reason` | yes | Why the workflow is being cancelled |
195
+ | `supervisor_id` | no | Supervisor ID for multi-supervisor mode |
196
+
197
+ ### Workflow tools
198
+
199
+ The remaining workflow tools read or drive workflow instances. Their argument lists are in [`docs/control-protocol.md`](../../docs/control-protocol.md).
200
+
201
+ | Tool | Purpose |
202
+ |---|---|
203
+ | `avenor_workflow_status` | Lightweight instance status |
204
+ | `avenor_workflow_wait` | Wait for a terminal state or a timeout |
205
+ | `avenor_workflow_inspect` | Full instance detail |
206
+ | `avenor_workflow_events` | The instance's log events |
207
+ | `avenor_workflow_complete` | Complete a machine/external handoff activation |
208
+ | `avenor_workflow_gate` | Record a gate decision on a parked activation |
209
+ | `avenor_workflow_controller_status` | Controller status, with an advisory candidate count |
210
+
158
211
  ### `avenor_shutdown`
159
212
 
160
213
  Shut down the avenor supervisor process and clean up temp files. Call this when you are done delegating work or if the supervisor has crashed.
@@ -172,7 +225,7 @@ Shut down the avenor supervisor process and clean up temp files. Call this when
172
225
  2. (avenor_answer_permission → if a permission is routed to this session mid-run)
173
226
  3. tool call returns → completion preview with status + session_id
174
227
  4. avenor_result → retrieve the complete final output when needed
175
- 5. avenor_follow_up → optionally iterate
228
+ 5. avenor_message → optionally iterate with action "resume"
176
229
  6. avenor_shutdown → clean up when finished
177
230
  ```
178
231
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as crypto from "node:crypto";
3
- import { Supervisor, THINKING_LEVELS, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, followUpTool, inspectTool, killTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, validateSpawnSelection, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
3
+ import { Supervisor, THINKING_LEVELS, answerPermissionTool, boundFinalOutput, createHostMessageProvider, createRunSnapshot, dial, eventsTool, extractEventText, extractTurnText, getSupervisorClient, inspectTool, killTool, messageTool, normalizeTurnStopReason, observeRun, peersDetails, peersTool, renderPeerList, resultOutput, resultTool, shutdownTool, spawnTool, statusTool, validateSpawnSelection, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
4
4
  //#region src/render.ts
5
5
  const FINAL_OUTPUT_CHARS$1 = 1500;
6
6
  const SCALAR_CHARS = 240;
@@ -121,7 +121,7 @@ function guidance(status, pending, runId) {
121
121
  const options = permissionOptions(pending);
122
122
  if (options.length > 0) return `Guidance: Call avenor_answer_permission with run_id "${runId}" and option_id "${selected(options[0], "option_id")}".`;
123
123
  if (pending) return `Guidance: Permission input is required, but no options are available; call avenor_inspect with run_id "${runId}".`;
124
- return `Guidance: Call avenor_follow_up or avenor_status with run_id "${runId}".`;
124
+ return `Guidance: Call avenor_message with action "resume" or avenor_status with run_id "${runId}".`;
125
125
  }
126
126
  if (isTerminal(status)) return `Guidance: Call avenor_result or avenor_events with run_id "${runId}".`;
127
127
  return `Guidance: Call avenor_result with run_id "${runId}".`;
@@ -212,14 +212,13 @@ function permissionLines(details, args) {
212
212
  `Guidance: Call avenor_status with run_id "${runId}" to verify the permission state.`
213
213
  ];
214
214
  }
215
- function followUpLines(details, args) {
216
- const result = asRecord(details);
217
- if (!result || !stringValue(result.run_id) || !stringValue(result.label)) return void 0;
218
- const runId = selected(result, "run_id", args.run_id);
219
- const label = selected(result, "label", args.label, runId);
215
+ function messageResumeLines(result, args) {
216
+ const runId = stringValue(result.runId);
217
+ const label = stringValue(result.label);
218
+ if (!runId || !label) return void 0;
220
219
  return [
221
- `Follow-up: ${label} — dispatched.`,
222
- `Source run: ${scalar(args.run_id)}`,
220
+ `Message: ${label} — resumed.`,
221
+ `Source run: ${scalar(args.target)}`,
223
222
  `Run: ${runId}`,
224
223
  `Label: ${label}`,
225
224
  `Guidance: Call avenor_status with run_id "${runId}".`
@@ -365,16 +364,30 @@ function formatAnswerPermissionOutput(args, result) {
365
364
  }
366
365
  };
367
366
  }
368
- function formatFollowUpOutput(args, result) {
369
- const record = result;
370
- const runId = selected(record, "run_id", args.run_id);
367
+ function formatMessageResumeOutput(args, result) {
368
+ const runId = stringValue(result.runId) ?? scalar(args.target);
371
369
  return {
372
- title: `${selected(record, "label", args.label, runId)} — dispatched`,
373
- output: output(followUpLines(result, args), "follow_up"),
374
- metadata: {
375
- ...result,
376
- source_run_id: args.run_id
377
- }
370
+ title: `${stringValue(result.label) ?? scalar(args.label) ?? runId} — resumed`,
371
+ output: output(messageResumeLines(result, args), "message"),
372
+ metadata: resultOutput(result)
373
+ };
374
+ }
375
+ /** Every message action's prose comes from the contract's `text`; only a resume
376
+ * keeps the structured acknowledgement lines the old follow-up tool rendered. */
377
+ function formatMessageOutput(args, result) {
378
+ if (args.action === "resume") return formatMessageResumeOutput(args, result);
379
+ return {
380
+ title: `Avenor ${stringValue(result.action) ?? scalar(args.action)} — ${stringValue(result.outcome) ?? "unknown"}`,
381
+ output: stringValue(result.text) ?? output(void 0, "message"),
382
+ metadata: resultOutput(result)
383
+ };
384
+ }
385
+ function formatPeersOutput(args, result) {
386
+ const text = result.action === "list" ? [result.text, renderPeerList(result.peers)].filter((part) => part !== "").join("\n\n") : result.text;
387
+ return {
388
+ title: `Avenor peers — ${result.action}`,
389
+ output: text || output(void 0, "peers"),
390
+ metadata: peersDetails(result)
378
391
  };
379
392
  }
380
393
  function formatEventsOutput(args, result) {
@@ -581,7 +594,7 @@ function buildWaitingText(run, snapshot) {
581
594
  else if (snapshot.phase_label) lines.push(`Question: ${snapshot.phase_label}`);
582
595
  const transcriptPreview = formatTranscriptPreview(snapshot);
583
596
  if (transcriptPreview) lines.push("", `Recent context:\n${transcriptPreview}`);
584
- if (!permission) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_status\` to re-check.`);
597
+ if (!permission) lines.push(`Use \`avenor_message\` with action "resume" and target "${run.runId}" to respond, or \`avenor_status\` to re-check.`);
585
598
  return lines.join("\n");
586
599
  }
587
600
  function buildCompletionText(run, snapshot, status, finalOutput) {
@@ -592,7 +605,7 @@ function buildCompletionText(run, snapshot, status, finalOutput) {
592
605
  if (sessionId) lines.push(`Session: \`${sessionId}\``);
593
606
  const boundedOutput = clipTail(finalOutput ?? snapshot.final_output, FINAL_OUTPUT_CHARS);
594
607
  if (boundedOutput) lines.push("", "Final output:", "```text", boundedOutput, "```");
595
- lines.push("", `Call \`avenor_events\` with \`run_id: "${run.runId}"\` for raw events, or \`avenor_follow_up\` to iterate.`);
608
+ lines.push("", `Call \`avenor_events\` with \`run_id: "${run.runId}"\` for raw events, or \`avenor_message\` with action "resume" to iterate.`);
596
609
  return lines.join("\n");
597
610
  }
598
611
  const CHANNEL_RE = /<channel\s+source="([^"]*)"(?:\s+from_run_id="([^"]*)")?(?:\s+from_role="([^"]*)")?[^>]*>([\s\S]*?)<\/channel>/g;
@@ -915,8 +928,148 @@ const AvenorPlugin = async (ctx) => {
915
928
  channelPolling = false;
916
929
  }
917
930
  }
931
+ let hostProvider;
932
+ let hostBinding;
933
+ let bindSeq = 0;
934
+ async function boundHostProvider() {
935
+ const { client, supervisorId } = await getSupervisorClient(void 0, { owner: true });
936
+ if (hostProvider && hostProvider.client === client) return hostProvider.binding;
937
+ const seq = ++bindSeq;
938
+ const preflight = await client.messagePreflight();
939
+ const stale = seq !== bindSeq;
940
+ if (stale && hostProvider) return hostProvider.binding;
941
+ const binding = {
942
+ provider: createHostMessageProvider({
943
+ surface: "opencode",
944
+ supervisorId,
945
+ generation: preflight.generation,
946
+ client
947
+ }),
948
+ supervisorId,
949
+ generation: preflight.generation
950
+ };
951
+ if (stale) return binding;
952
+ hostProvider = {
953
+ client,
954
+ binding
955
+ };
956
+ if (hostBinding) hostBinding = {
957
+ ...binding,
958
+ sessionId: hostBinding.sessionId
959
+ };
960
+ return binding;
961
+ }
962
+ async function hostMessageProvider(sessionId) {
963
+ const bound = await boundHostProvider();
964
+ if (!hostBinding) hostBinding = {
965
+ ...bound,
966
+ sessionId
967
+ };
968
+ else if (hostBinding.sessionId !== sessionId) console.error(`opencode: host session already bound to ${hostBinding.sessionId}`);
969
+ return {
970
+ ...bound.provider,
971
+ onResumed: (result) => trackResumed(sessionId, result)
972
+ };
973
+ }
974
+ async function trackResumed(sessionId, result) {
975
+ const run = {
976
+ runId: result.runId ?? "",
977
+ orchestratorSessionId: sessionId,
978
+ label: result.label ?? result.runId ?? "",
979
+ supervisorId: hostBinding?.supervisorId,
980
+ monitoring: false
981
+ };
982
+ trackedRuns.set(run.runId, run);
983
+ await primeTrackedRun(run);
984
+ monitorRun(run).catch(console.error);
985
+ }
986
+ let publishedTurn;
987
+ let publishChain = Promise.resolve();
988
+ function isNewerTurn(created, id) {
989
+ if (!publishedTurn) return true;
990
+ if (created !== publishedTurn.created) return created > publishedTurn.created;
991
+ return id > publishedTurn.id;
992
+ }
993
+ /** The raw stop reason one settled OpenCode assistant message reports. */
994
+ function stopReasonOf(info) {
995
+ const error = info.error;
996
+ if (error !== null && typeof error === "object") {
997
+ const name = error.name;
998
+ if (name === "MessageAbortedError") return "aborted";
999
+ if (name === "MessageOutputLengthError") return "length";
1000
+ return "error";
1001
+ }
1002
+ const finish = typeof info.finish === "string" ? info.finish.trim() : "";
1003
+ return finish === "" ? "end_turn" : finish;
1004
+ }
1005
+ /**
1006
+ * Translate the ai-sdk finish spellings onto the words the shared normaliser
1007
+ * knows. They are adapter vocabulary, not pi spellings, so they are mapped
1008
+ * here rather than in the shared table a Pi child's session.end also uses.
1009
+ */
1010
+ function normalizeFinish(stopReason) {
1011
+ switch (stopReason) {
1012
+ case "tool-calls":
1013
+ case "tool_calls":
1014
+ case "toolUse": return "tool_use";
1015
+ case "end-turn": return "end_of_turn";
1016
+ default: return stopReason;
1017
+ }
1018
+ }
1019
+ /**
1020
+ * The text parts of one assistant message, read by id. The read is bounded to
1021
+ * the message the event carried, never the session's whole history.
1022
+ */
1023
+ async function settledTurnText(sessionId, messageId) {
1024
+ const response = await ctx.client.session.message({ path: {
1025
+ id: sessionId,
1026
+ messageID: messageId
1027
+ } });
1028
+ return (Array.isArray(response.data?.parts) ? response.data.parts : []).filter((part) => part !== null && typeof part === "object").filter((part) => part.type === "text").map((part) => part.text ?? "");
1029
+ }
1030
+ async function publishHostTurn(info) {
1031
+ const binding = hostBinding;
1032
+ if (!binding) return;
1033
+ if (info.role !== "assistant") return;
1034
+ if (info.sessionID !== binding.sessionId) return;
1035
+ if (info.summary === true) return;
1036
+ const time = info.time;
1037
+ if (typeof time?.completed !== "number") return;
1038
+ const created = typeof time.created === "number" ? time.created : 0;
1039
+ const messageId = typeof info.id === "string" ? info.id : "";
1040
+ if (messageId === "") return;
1041
+ if (!binding.provider.recordLastTurn) return;
1042
+ const publish = publishChain.then(() => publishTurn(binding, info, created, messageId));
1043
+ publishChain = publish.catch(() => {});
1044
+ await publish;
1045
+ }
1046
+ /**
1047
+ * One queued publish. The newest-turn gate is read here rather than before
1048
+ * queueing, so it sees the turn the publish ahead of it claimed.
1049
+ */
1050
+ async function publishTurn(binding, info, created, messageId) {
1051
+ if (!isNewerTurn(created, messageId)) return;
1052
+ try {
1053
+ const turn = extractTurnText([{
1054
+ content: await settledTurnText(binding.sessionId, messageId),
1055
+ stopReason: normalizeTurnStopReason(normalizeFinish(stopReasonOf(info)))
1056
+ }]);
1057
+ const bounded = boundFinalOutput(turn.text);
1058
+ await binding.provider.recordLastTurn(bounded, turn.stopReason, turn.text.length > bounded.length);
1059
+ publishedTurn = {
1060
+ created,
1061
+ id: messageId
1062
+ };
1063
+ } catch (error) {
1064
+ console.error("[avenor] session_last_set failed:", error instanceof Error ? error.message : error);
1065
+ }
1066
+ }
918
1067
  return {
919
1068
  event: async ({ event }) => {
1069
+ if (event.type === "message.updated") {
1070
+ await publishHostTurn(event.properties.info);
1071
+ return;
1072
+ }
920
1073
  if (event.type !== "session.idle") return;
921
1074
  const { sessionID } = event.properties;
922
1075
  pollChannelMessages(sessionID).catch(console.error);
@@ -1140,34 +1293,64 @@ const AvenorPlugin = async (ctx) => {
1140
1293
  }));
1141
1294
  }
1142
1295
  }),
1143
- avenor_follow_up: tool({
1144
- description: "Resume a completed run with a follow-up message.",
1296
+ avenor_message: tool({
1297
+ description: "Send a message to another agent run, withdraw a pending ask, resume a completed run, or deliver a control reply. Every action is explicit and the result states whether a failed ask can still be withdrawn. `reply` always reports edge-absent here: this session receives no inbound asks, so nothing is sent.",
1145
1298
  args: {
1146
- run_id: tool.schema.string().describe("Completed run ID to resume"),
1147
- message: tool.schema.string().describe("Follow-up message text"),
1148
- label: tool.schema.string().optional().describe("Override label (defaults to <original>-followup)"),
1149
- supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
1299
+ action: tool.schema.enum([
1300
+ "tell",
1301
+ "ask",
1302
+ "reply",
1303
+ "resume",
1304
+ "withdraw",
1305
+ "control_reply"
1306
+ ]).describe("tell | ask | reply | resume | withdraw | control_reply"),
1307
+ target: tool.schema.string().optional().describe("Recipient for tell/ask/resume (a run id, a peer label, or \"self\"), or the opaque control message id for control_reply"),
1308
+ message: tool.schema.string().optional().describe("Message text for tell/ask/reply/resume"),
1309
+ reply_to: tool.schema.string().optional().describe("Inbound ask id being answered (reply)"),
1310
+ message_id: tool.schema.string().optional().describe("Ask id being withdrawn (withdraw)"),
1311
+ timeout_ms: tool.schema.number().int().optional().describe("Bound on the ask wait in milliseconds"),
1312
+ label: tool.schema.string().optional().describe("Override label for the resumed run (resume)"),
1313
+ idempotency_key: tool.schema.string().optional().describe("Forwarded verbatim to the resume authority (resume)"),
1314
+ payload: tool.schema.unknown().optional().describe("Control reply body (control_reply)")
1150
1315
  },
1151
1316
  async execute(args, context) {
1152
- const result = await followUpTool({
1153
- runId: args.run_id,
1154
- message: args.message,
1155
- label: args.label,
1156
- supervisorId: args.supervisor_id
1317
+ return formatMessageOutput(args, await messageTool(args, await hostMessageProvider(context.sessionID), { signal: context.abort }));
1318
+ }
1319
+ }),
1320
+ avenor_peers: tool({
1321
+ description: "List the visible agent sessions, rename a run, or read a run's last agent message. `last` returns the target's final text with its stop reason, or an empty result when the target has no completed turn yet.",
1322
+ args: {
1323
+ action: tool.schema.enum([
1324
+ "list",
1325
+ "rename",
1326
+ "last"
1327
+ ]).optional().describe("list (the default) | rename | last"),
1328
+ name: tool.schema.string().optional().describe("New display name (rename)"),
1329
+ session_id: tool.schema.string().optional().describe("Run to rename; omitted renames the caller (rename)"),
1330
+ target: tool.schema.string().optional().describe("Peer to read: a run id, a label, or \"self\" (last)")
1331
+ },
1332
+ async execute(args, context) {
1333
+ return formatPeersOutput(args, await peersTool(args, await hostMessageProvider(context.sessionID)));
1334
+ }
1335
+ }),
1336
+ avenor_workflow_cancel: tool({
1337
+ description: "Cancel a workflow instance. The actor and reason are required.",
1338
+ args: {
1339
+ workflow_id: tool.schema.string().describe("Workflow ID"),
1340
+ actor: tool.schema.string().describe("Who is cancelling"),
1341
+ reason: tool.schema.string().describe("Why the workflow is being cancelled"),
1342
+ supervisor_id: tool.schema.string().optional().describe("Supervisor ID for multi-supervisor mode")
1343
+ },
1344
+ async execute(args, _context) {
1345
+ const { supervisor_id, ...rest } = args;
1346
+ const result = await workflowCancelTool({
1347
+ ...rest,
1348
+ supervisorId: supervisor_id
1157
1349
  });
1158
- const priorRun = trackedRuns.get(args.run_id);
1159
- const run = {
1160
- runId: result.run_id,
1161
- agent: priorRun?.agent ?? "agent",
1162
- orchestratorSessionId: context.sessionID,
1163
- label: result.label,
1164
- supervisorId: args.supervisor_id ?? Supervisor.currentInstance()?.supervisorId,
1165
- monitoring: false
1350
+ return {
1351
+ title: "workflow",
1352
+ output: JSON.stringify(result, null, 2)
1166
1353
  };
1167
- trackedRuns.set(result.run_id, run);
1168
- await primeTrackedRun(run);
1169
- monitorRun(run).catch(console.error);
1170
- return formatFollowUpOutput(args, result);
1171
1354
  }
1172
1355
  }),
1173
1356
  avenor_events: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-opencode",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "dev": "tsdown --watch"
28
28
  },
29
29
  "dependencies": {
30
- "@dougbots/avenor-core": "^0.14.0"
30
+ "@dougbots/avenor-core": "^0.16.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@opencode-ai/plugin": "1.15.3"