@dougbots/avenor-opencode 0.15.0 → 0.16.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.
- package/README.md +63 -10
- package/dist/index.js +226 -43
- 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
|
|
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`, `
|
|
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
|
-
### `
|
|
137
|
+
### `avenor_message`
|
|
137
138
|
|
|
138
|
-
|
|
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
|
-
|
|
|
141
|
+
| `action` | Required fields | What it does |
|
|
141
142
|
|---|---|---|
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
`
|
|
222
|
-
`Source run: ${scalar(args.
|
|
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
|
|
369
|
-
const
|
|
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: `${
|
|
373
|
-
output: output(
|
|
374
|
-
metadata:
|
|
375
|
-
|
|
376
|
-
|
|
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 \`
|
|
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 \`
|
|
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
|
-
|
|
1144
|
-
description: "
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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.
|
|
3
|
+
"version": "0.16.1",
|
|
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.
|
|
30
|
+
"@dougbots/avenor-core": "^0.16.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@opencode-ai/plugin": "1.15.3"
|