opencode-multiagent 0.2.1 → 0.4.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.
- package/AGENTS.md +83 -0
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +36 -0
- package/README.md +44 -168
- package/README.tr.md +84 -0
- package/RELEASE.md +68 -0
- package/agents/AGENTS.md +91 -0
- package/agents/auditor.md +67 -23
- package/agents/{worker.md → coder.md} +24 -17
- package/agents/docmaster.md +91 -0
- package/agents/executor.md +63 -79
- package/agents/planner.md +78 -58
- package/agents/reviewer.md +31 -15
- package/agents/scout.md +25 -17
- package/agents/sec-coder.md +83 -0
- package/agents/ui-coder.md +77 -0
- package/commands/board.md +17 -0
- package/commands/execute.md +9 -7
- package/commands/init-deep.md +7 -6
- package/commands/init.md +5 -5
- package/commands/inspect.md +6 -5
- package/commands/plan.md +8 -6
- package/commands/quality.md +4 -3
- package/commands/review.md +5 -3
- package/commands/status.md +5 -3
- package/defaults/AGENTS.md +48 -0
- package/defaults/opencode-multiagent.json +180 -0
- package/defaults/opencode-multiagent.schema.json +265 -0
- package/dist/control-plane.d.ts +4 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1916 -0
- package/dist/opencode-multiagent/compiler.d.ts +25 -0
- package/dist/opencode-multiagent/compiler.d.ts.map +1 -0
- package/dist/opencode-multiagent/constants.d.ts +128 -0
- package/dist/opencode-multiagent/constants.d.ts.map +1 -0
- package/dist/opencode-multiagent/correlation.d.ts +21 -0
- package/dist/opencode-multiagent/correlation.d.ts.map +1 -0
- package/dist/opencode-multiagent/defaults.d.ts +10 -0
- package/dist/opencode-multiagent/defaults.d.ts.map +1 -0
- package/dist/opencode-multiagent/hooks.d.ts +62 -0
- package/dist/opencode-multiagent/hooks.d.ts.map +1 -0
- package/dist/opencode-multiagent/log.d.ts +2 -0
- package/dist/opencode-multiagent/log.d.ts.map +1 -0
- package/dist/opencode-multiagent/markdown.d.ts +8 -0
- package/dist/opencode-multiagent/markdown.d.ts.map +1 -0
- package/dist/opencode-multiagent/mcp.d.ts +3 -0
- package/dist/opencode-multiagent/mcp.d.ts.map +1 -0
- package/dist/opencode-multiagent/policy.d.ts +5 -0
- package/dist/opencode-multiagent/policy.d.ts.map +1 -0
- package/dist/opencode-multiagent/quality.d.ts +18 -0
- package/dist/opencode-multiagent/quality.d.ts.map +1 -0
- package/dist/opencode-multiagent/runtime.d.ts +7 -0
- package/dist/opencode-multiagent/runtime.d.ts.map +1 -0
- package/dist/opencode-multiagent/session-tracker.d.ts +32 -0
- package/dist/opencode-multiagent/session-tracker.d.ts.map +1 -0
- package/dist/opencode-multiagent/skills.d.ts +17 -0
- package/dist/opencode-multiagent/skills.d.ts.map +1 -0
- package/dist/opencode-multiagent/supervision.d.ts +26 -0
- package/dist/opencode-multiagent/supervision.d.ts.map +1 -0
- package/dist/opencode-multiagent/task-manager.d.ts +54 -0
- package/dist/opencode-multiagent/task-manager.d.ts.map +1 -0
- package/dist/opencode-multiagent/telemetry.d.ts +28 -0
- package/dist/opencode-multiagent/telemetry.d.ts.map +1 -0
- package/dist/opencode-multiagent/tools.d.ts +87 -0
- package/dist/opencode-multiagent/tools.d.ts.map +1 -0
- package/dist/opencode-multiagent/types.d.ts +36 -0
- package/dist/opencode-multiagent/types.d.ts.map +1 -0
- package/dist/opencode-multiagent/utils.d.ts +9 -0
- package/dist/opencode-multiagent/utils.d.ts.map +1 -0
- package/docs/agents.md +148 -0
- package/docs/agents.tr.md +149 -0
- package/docs/configuration.md +244 -0
- package/docs/configuration.tr.md +244 -0
- package/docs/usage-guide.md +224 -0
- package/docs/usage-guide.tr.md +225 -0
- package/examples/opencode.with-overrides.json +3 -7
- package/package.json +23 -13
- package/skills/AGENTS.md +51 -0
- package/skills/advanced-evaluation/SKILL.md +37 -21
- package/skills/advanced-evaluation/manifest.json +2 -13
- package/skills/cek-context-engineering/SKILL.md +159 -87
- package/skills/cek-context-engineering/manifest.json +1 -3
- package/skills/cek-prompt-engineering/SKILL.md +13 -10
- package/skills/cek-prompt-engineering/manifest.json +1 -3
- package/skills/cek-test-prompt/SKILL.md +38 -28
- package/skills/cek-test-prompt/manifest.json +1 -3
- package/skills/cek-thought-based-reasoning/SKILL.md +75 -21
- package/skills/cek-thought-based-reasoning/manifest.json +1 -3
- package/skills/context-degradation/SKILL.md +14 -13
- package/skills/context-degradation/manifest.json +1 -3
- package/skills/debate/SKILL.md +23 -78
- package/skills/debate/manifest.json +2 -12
- package/skills/design-first/manifest.json +2 -13
- package/skills/dispatching-parallel-agents/SKILL.md +14 -3
- package/skills/dispatching-parallel-agents/manifest.json +1 -4
- package/skills/drift-analysis/SKILL.md +50 -29
- package/skills/drift-analysis/manifest.json +2 -12
- package/skills/evaluation/manifest.json +2 -12
- package/skills/executing-plans/SKILL.md +15 -8
- package/skills/executing-plans/manifest.json +1 -3
- package/skills/handoff-protocols/manifest.json +2 -12
- package/skills/parallel-investigation/SKILL.md +25 -12
- package/skills/parallel-investigation/manifest.json +1 -4
- package/skills/reflexion-critique/SKILL.md +21 -10
- package/skills/reflexion-critique/manifest.json +1 -3
- package/skills/reflexion-reflect/SKILL.md +36 -34
- package/skills/reflexion-reflect/manifest.json +2 -10
- package/skills/root-cause-analysis/manifest.json +2 -13
- package/skills/sadd-judge-with-debate/SKILL.md +50 -26
- package/skills/sadd-judge-with-debate/manifest.json +1 -3
- package/skills/structured-code-review/manifest.json +2 -11
- package/skills/task-decomposition/manifest.json +2 -13
- package/skills/verification-before-completion/manifest.json +2 -15
- package/skills/verification-gates/SKILL.md +27 -19
- package/skills/verification-gates/manifest.json +2 -12
- package/agents/advisor.md +0 -57
- package/agents/critic.md +0 -127
- package/agents/deep-worker.md +0 -65
- package/agents/devil.md +0 -36
- package/agents/heavy-worker.md +0 -68
- package/agents/lead.md +0 -155
- package/agents/librarian.md +0 -62
- package/agents/qa.md +0 -50
- package/agents/quick.md +0 -65
- package/agents/scribe.md +0 -78
- package/agents/strategist.md +0 -63
- package/agents/ui-heavy-worker.md +0 -62
- package/agents/ui-worker.md +0 -69
- package/agents/validator.md +0 -47
- package/defaults/agent-settings.json +0 -102
- package/defaults/agent-settings.schema.json +0 -25
- package/defaults/flags.json +0 -35
- package/defaults/flags.schema.json +0 -119
- package/defaults/mcp-defaults.json +0 -47
- package/defaults/mcp-defaults.schema.json +0 -38
- package/defaults/profiles.json +0 -53
- package/defaults/profiles.schema.json +0 -60
- package/defaults/team-profiles.json +0 -83
- package/src/control-plane.ts +0 -21
- package/src/index.ts +0 -8
- package/src/opencode-multiagent/compiler.ts +0 -168
- package/src/opencode-multiagent/constants.ts +0 -178
- package/src/opencode-multiagent/file-lock.ts +0 -90
- package/src/opencode-multiagent/hooks.ts +0 -599
- package/src/opencode-multiagent/log.ts +0 -12
- package/src/opencode-multiagent/mailbox.ts +0 -287
- package/src/opencode-multiagent/markdown.ts +0 -99
- package/src/opencode-multiagent/mcp.ts +0 -35
- package/src/opencode-multiagent/policy.ts +0 -67
- package/src/opencode-multiagent/quality.ts +0 -140
- package/src/opencode-multiagent/runtime.ts +0 -55
- package/src/opencode-multiagent/skills.ts +0 -144
- package/src/opencode-multiagent/supervision.ts +0 -156
- package/src/opencode-multiagent/task-manager.ts +0 -148
- package/src/opencode-multiagent/team-manager.ts +0 -219
- package/src/opencode-multiagent/team-tools.ts +0 -359
- package/src/opencode-multiagent/telemetry.ts +0 -124
- package/src/opencode-multiagent/utils.ts +0 -54
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
|
|
3
|
-
import type { Hooks } from "@opencode-ai/plugin";
|
|
4
|
-
import { tool as pluginTool } from "@opencode-ai/plugin";
|
|
5
|
-
import type { Config } from "@opencode-ai/sdk";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
applyBuiltInAgentPolicy,
|
|
9
|
-
applyPermissionDefaults,
|
|
10
|
-
compileAgents,
|
|
11
|
-
compileCommands,
|
|
12
|
-
type MergedConfig,
|
|
13
|
-
type MpcPermissionRegistry,
|
|
14
|
-
} from "./compiler.ts";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extends the official SDK Hooks interface with additional hooks that OpenCode
|
|
18
|
-
* supports at runtime but does not yet document in its published types.
|
|
19
|
-
*/
|
|
20
|
-
export type ExtendedHooks = Hooks & {
|
|
21
|
-
/** Called at plugin teardown to release resources. */
|
|
22
|
-
cleanup?: () => void;
|
|
23
|
-
/** Inject custom HTTP headers into every LLM request. */
|
|
24
|
-
"chat.headers"?: (
|
|
25
|
-
input: { sessionID: string; agent?: unknown },
|
|
26
|
-
output: { headers: Record<string, string> },
|
|
27
|
-
) => Promise<void>;
|
|
28
|
-
/** Observe or block a command before it is executed. */
|
|
29
|
-
"command.execute.before"?: (input: {
|
|
30
|
-
sessionID: string;
|
|
31
|
-
command: string;
|
|
32
|
-
arguments: string;
|
|
33
|
-
}) => Promise<void>;
|
|
34
|
-
/** Modify a built-in tool's definition (e.g. append policy notes). */
|
|
35
|
-
"tool.definition"?: (
|
|
36
|
-
input: { toolID: string },
|
|
37
|
-
output: { description: string },
|
|
38
|
-
) => Promise<void>;
|
|
39
|
-
/** Inject environment variables into every shell invocation. */
|
|
40
|
-
"shell.env"?: (
|
|
41
|
-
input: unknown,
|
|
42
|
-
output: { env: Record<string, string> },
|
|
43
|
-
) => Promise<void>;
|
|
44
|
-
/** Append text to the system prompt (experimental). */
|
|
45
|
-
"experimental.chat.system.transform"?: (
|
|
46
|
-
input: unknown,
|
|
47
|
-
output: { system: string[] },
|
|
48
|
-
) => Promise<void>;
|
|
49
|
-
/** Mutate the chat message list sent to the LLM (experimental). */
|
|
50
|
-
"experimental.chat.messages.transform"?: (
|
|
51
|
-
input: unknown,
|
|
52
|
-
output: { messages: Array<{ info?: { role?: string }; parts: unknown[] }> },
|
|
53
|
-
) => Promise<void>;
|
|
54
|
-
/** Inject context into session compaction summaries (experimental). */
|
|
55
|
-
"experimental.session.compacting"?: (
|
|
56
|
-
input: unknown,
|
|
57
|
-
output: { context: string[] },
|
|
58
|
-
) => Promise<void>;
|
|
59
|
-
/** Append text to completed LLM responses (experimental). */
|
|
60
|
-
"experimental.text.complete"?: (
|
|
61
|
-
input: unknown,
|
|
62
|
-
output: { text: string },
|
|
63
|
-
) => Promise<void>;
|
|
64
|
-
};
|
|
65
|
-
import {
|
|
66
|
-
bundledAgentsDir,
|
|
67
|
-
bundledCommandsDir,
|
|
68
|
-
globalAgentsDir,
|
|
69
|
-
globalCommandsDir,
|
|
70
|
-
mcpToolPrefixes,
|
|
71
|
-
pluginMode,
|
|
72
|
-
trackedEventTypes,
|
|
73
|
-
} from "./constants.ts";
|
|
74
|
-
import { createFileLockController } from "./file-lock.ts";
|
|
75
|
-
import { note } from "./log.ts";
|
|
76
|
-
import { applyMcpDefaults, loadMcpDefaults } from "./mcp.ts";
|
|
77
|
-
import { blocked, experimentalText, flagged, risky, tokenizedBashBlocked } from "./policy.ts";
|
|
78
|
-
import { createQualityController } from "./quality.ts";
|
|
79
|
-
import { resolveExistingTierReferences } from "./runtime.ts";
|
|
80
|
-
import { loadSkillRegistry } from "./skills.ts";
|
|
81
|
-
import { createMailbox, createMailboxEventHandler } from "./mailbox.ts";
|
|
82
|
-
import { createSupervisionController } from "./supervision.ts";
|
|
83
|
-
import { createTaskManager } from "./task-manager.ts";
|
|
84
|
-
import { createTeamManager, loadTeamProfiles } from "./team-manager.ts";
|
|
85
|
-
import { createTeamTools } from "./team-tools.ts";
|
|
86
|
-
import { createTelemetryController } from "./telemetry.ts";
|
|
87
|
-
import { clip, compact, label, text } from "./utils.ts";
|
|
88
|
-
|
|
89
|
-
type GenericRecord = Record<string, any>;
|
|
90
|
-
|
|
91
|
-
const isMcpTool = (tool: unknown): tool is string =>
|
|
92
|
-
typeof tool === "string" && mcpToolPrefixes.some((prefix) => tool.startsWith(prefix));
|
|
93
|
-
|
|
94
|
-
const qaWarningThreshold = 3;
|
|
95
|
-
const qaWarningCooldownMs = 5 * 60 * 1000;
|
|
96
|
-
|
|
97
|
-
const getTaskTarget = (args: GenericRecord = {}): string | undefined => {
|
|
98
|
-
for (const key of ["subagent_type", "agent", "subagent", "type", "name"]) {
|
|
99
|
-
if (typeof args?.[key] === "string") return args[key];
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const matchesMcpPermission = (tool: string, registry?: MpcPermissionRegistry): boolean => {
|
|
105
|
-
if (!registry) return true;
|
|
106
|
-
for (const pattern of registry.denied ?? []) {
|
|
107
|
-
if (pattern.endsWith("*") && tool.startsWith(pattern.slice(0, -1))) return false;
|
|
108
|
-
if (pattern === tool) return false;
|
|
109
|
-
}
|
|
110
|
-
for (const pattern of registry.allowed ?? []) {
|
|
111
|
-
if (pattern.endsWith("*") && tool.startsWith(pattern.slice(0, -1))) return true;
|
|
112
|
-
if (pattern === tool) return true;
|
|
113
|
-
}
|
|
114
|
-
return registry.fallback === "allow";
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export const createPluginHooks = ({
|
|
118
|
-
client,
|
|
119
|
-
flags,
|
|
120
|
-
modelTiers,
|
|
121
|
-
agentSettings,
|
|
122
|
-
projectRoot,
|
|
123
|
-
}: {
|
|
124
|
-
client: any;
|
|
125
|
-
flags: GenericRecord;
|
|
126
|
-
modelTiers: Record<string, string>;
|
|
127
|
-
agentSettings: GenericRecord;
|
|
128
|
-
projectRoot?: string;
|
|
129
|
-
}): ExtendedHooks => {
|
|
130
|
-
const projectAgentsDir = projectRoot ? join(projectRoot, ".opencode", "agents") : undefined;
|
|
131
|
-
const projectCommandsDir = projectRoot ? join(projectRoot, ".opencode", "commands") : undefined;
|
|
132
|
-
const { handleSupervision, cleanup } = createSupervisionController({ flags, client });
|
|
133
|
-
const quality = createQualityController({ flags, client });
|
|
134
|
-
const telemetry = createTelemetryController({ flags });
|
|
135
|
-
const fileLocks = createFileLockController();
|
|
136
|
-
const sessionAgentMap = new Map<string, GenericRecord>();
|
|
137
|
-
const qaDispatchState = new Map<string, { count: number; remindedAt: number }>();
|
|
138
|
-
let skillRegistry: Map<string, any> | null = null;
|
|
139
|
-
let mcpDefaults: Record<string, unknown> | null = null;
|
|
140
|
-
let mcpRegistry: Map<string, MpcPermissionRegistry> | null = null;
|
|
141
|
-
const taskManager = createTaskManager(projectRoot);
|
|
142
|
-
void taskManager.load();
|
|
143
|
-
const mailbox = createMailbox();
|
|
144
|
-
const teamManager = createTeamManager();
|
|
145
|
-
/** Tracks parent session ID and agent name for each child session — used by mailboxHandler. */
|
|
146
|
-
const childMailboxInfo = new Map<string, { parentID: string; agentName: string | null }>();
|
|
147
|
-
const mailboxHandler = createMailboxEventHandler(mailbox, client, (sid) => childMailboxInfo.get(sid));
|
|
148
|
-
|
|
149
|
-
const rememberSession = (sessionID: string | undefined, extra: GenericRecord = {}): void => {
|
|
150
|
-
if (!sessionID) return;
|
|
151
|
-
const current = sessionAgentMap.get(sessionID) ?? {};
|
|
152
|
-
const next = { ...current, ...extra };
|
|
153
|
-
sessionAgentMap.set(sessionID, next);
|
|
154
|
-
telemetry.rememberSession(sessionID, next);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const trackQaDispatch = async (sessionID: string | undefined, agentInfo: GenericRecord = {}): Promise<void> => {
|
|
158
|
-
if (!sessionID) return;
|
|
159
|
-
const state = qaDispatchState.get(sessionID) ?? { count: 0, remindedAt: 0 };
|
|
160
|
-
state.count += 1;
|
|
161
|
-
qaDispatchState.set(sessionID, state);
|
|
162
|
-
if (state.count < qaWarningThreshold || !client?.session?.prompt) return;
|
|
163
|
-
const now = Date.now();
|
|
164
|
-
if (now - state.remindedAt < qaWarningCooldownMs) return;
|
|
165
|
-
state.remindedAt = now;
|
|
166
|
-
try {
|
|
167
|
-
await client.session.prompt({
|
|
168
|
-
path: { id: sessionID },
|
|
169
|
-
body: {
|
|
170
|
-
noReply: true,
|
|
171
|
-
parts: [
|
|
172
|
-
{
|
|
173
|
-
type: "text",
|
|
174
|
-
text:
|
|
175
|
-
`[opencode-multiagent qa] This session has dispatched QA ${state.count} times. ` +
|
|
176
|
-
"Reassess the brief, consider planner repair, or narrow the defect before sending QA again.",
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
} catch {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
await note("qa_counter_warning", {
|
|
185
|
-
observation: true,
|
|
186
|
-
sessionID,
|
|
187
|
-
agent: agentInfo.agent,
|
|
188
|
-
model: agentInfo.model,
|
|
189
|
-
qa_dispatch_count: state.count,
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
cleanup(): void {
|
|
195
|
-
cleanup?.();
|
|
196
|
-
quality.cleanup?.();
|
|
197
|
-
telemetry.cleanup?.();
|
|
198
|
-
fileLocks.cleanup?.();
|
|
199
|
-
teamManager.cleanup();
|
|
200
|
-
childMailboxInfo.clear();
|
|
201
|
-
},
|
|
202
|
-
async event(input: any): Promise<void> {
|
|
203
|
-
const type = input.event?.type;
|
|
204
|
-
const props = input.event?.properties ?? {};
|
|
205
|
-
|
|
206
|
-
if (flags.observation && trackedEventTypes.has(type)) {
|
|
207
|
-
await note(
|
|
208
|
-
"event",
|
|
209
|
-
compact({
|
|
210
|
-
event: type,
|
|
211
|
-
data: compact({
|
|
212
|
-
sessionID:
|
|
213
|
-
typeof props.sessionID === "string"
|
|
214
|
-
? props.sessionID
|
|
215
|
-
: typeof props.info?.sessionID === "string"
|
|
216
|
-
? props.info.sessionID
|
|
217
|
-
: undefined,
|
|
218
|
-
messageID: typeof props.messageID === "string" ? props.messageID : undefined,
|
|
219
|
-
partID: typeof props.partID === "string" ? props.partID : undefined,
|
|
220
|
-
permissionID: typeof props.permissionID === "string" ? props.permissionID : undefined,
|
|
221
|
-
response: typeof props.response === "string" ? props.response : undefined,
|
|
222
|
-
file: typeof props.file === "string" ? clip(props.file) : undefined,
|
|
223
|
-
error: typeof props.error?.message === "string" ? clip(props.error.message) : undefined,
|
|
224
|
-
}),
|
|
225
|
-
}),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (type === "session.created") {
|
|
230
|
-
const parentID = typeof props.info?.parentID === "string" ? props.info.parentID : undefined;
|
|
231
|
-
const childID = typeof props.sessionID === "string" ? props.sessionID : typeof props.info?.id === "string" ? props.info.id : undefined;
|
|
232
|
-
if (parentID && childID) {
|
|
233
|
-
const parent = sessionAgentMap.get(parentID) ?? {};
|
|
234
|
-
telemetry.trackTaskDispatch(parentID, { agent: parent.agent, model: parent.model });
|
|
235
|
-
await note("task_dispatch", {
|
|
236
|
-
observation: true,
|
|
237
|
-
parent_sessionID: parentID,
|
|
238
|
-
parent_agent: parent.agent,
|
|
239
|
-
child_sessionID: childID,
|
|
240
|
-
});
|
|
241
|
-
// Track for mailbox delivery and lead notification.
|
|
242
|
-
childMailboxInfo.set(childID, { parentID, agentName: null });
|
|
243
|
-
// Mark team member as active if it was registered by team_create.
|
|
244
|
-
teamManager.setMemberStatus(childID, "active");
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (type === "message.updated") {
|
|
249
|
-
const sessionID = typeof props.info?.sessionID === "string" ? props.info.sessionID : undefined;
|
|
250
|
-
const agent = typeof props.info?.agent === "string" ? props.info.agent : undefined;
|
|
251
|
-
if (sessionID && agent) {
|
|
252
|
-
rememberSession(sessionID, { agent });
|
|
253
|
-
// Keep childMailboxInfo agent name up-to-date for lead notifications.
|
|
254
|
-
const mailboxChild = childMailboxInfo.get(sessionID);
|
|
255
|
-
if (mailboxChild) mailboxChild.agentName = agent;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (type === "session.deleted") {
|
|
260
|
-
const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
|
|
261
|
-
if (sessionID) {
|
|
262
|
-
sessionAgentMap.delete(sessionID);
|
|
263
|
-
qaDispatchState.delete(sessionID);
|
|
264
|
-
fileLocks.releaseAll(sessionID);
|
|
265
|
-
await telemetry.flushSession(sessionID);
|
|
266
|
-
teamManager.remove(sessionID);
|
|
267
|
-
// Note: childMailboxInfo is intentionally NOT cleaned here so that
|
|
268
|
-
// mailboxHandler (called below) can read the parent info for lead notification.
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
await handleSupervision(input.event);
|
|
273
|
-
await quality.handleQualityEvent(input.event);
|
|
274
|
-
// Deliver pending mailbox messages on idle; notify lead on child session deletion.
|
|
275
|
-
await mailboxHandler(input.event);
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
async "permission.ask"(input: any, output: any): Promise<void> {
|
|
279
|
-
if (!flags.enforcement) return;
|
|
280
|
-
const patternValue = Array.isArray(input.pattern) ? input.pattern.join(" ") : String(input.pattern ?? "");
|
|
281
|
-
if (["read", "edit", "external_directory"].includes(input.type) && blocked(patternValue)) {
|
|
282
|
-
output.status = "deny";
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (input.type === "bash" && tokenizedBashBlocked(patternValue)) {
|
|
286
|
-
output.status = "deny";
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
async "chat.params"(input: any, output: any): Promise<void> {
|
|
291
|
-
rememberSession(input.sessionID, {
|
|
292
|
-
agent: label(input.agent),
|
|
293
|
-
model: label(input.model, ["id", "name", "modelID"]),
|
|
294
|
-
});
|
|
295
|
-
if (!flags.observation) return;
|
|
296
|
-
const marks: string[] = [];
|
|
297
|
-
if (typeof output.temperature === "number" && output.temperature > 1) marks.push("high_temperature");
|
|
298
|
-
if (typeof output.topK === "number" && output.topK > 0) marks.push("nonzero_top_k");
|
|
299
|
-
if (Object.keys(output.options ?? {}).length > 0) marks.push("non_empty_options");
|
|
300
|
-
await note(
|
|
301
|
-
"chat_params",
|
|
302
|
-
compact({
|
|
303
|
-
observation: true,
|
|
304
|
-
sessionID: input.sessionID,
|
|
305
|
-
agent: label(input.agent),
|
|
306
|
-
provider: compact({
|
|
307
|
-
source: input.provider?.source,
|
|
308
|
-
id: label(input.provider?.info, ["id", "name", "providerID"]),
|
|
309
|
-
}),
|
|
310
|
-
model: compact({
|
|
311
|
-
id: label(input.model, ["id", "name", "modelID"]),
|
|
312
|
-
providerID: label(input.model, ["providerID"]),
|
|
313
|
-
}),
|
|
314
|
-
params: compact({
|
|
315
|
-
temperature: output.temperature,
|
|
316
|
-
topP: output.topP,
|
|
317
|
-
topK: output.topK,
|
|
318
|
-
optionKeys: Object.keys(output.options ?? {}),
|
|
319
|
-
}),
|
|
320
|
-
heuristic: marks.length > 0 ? marks : undefined,
|
|
321
|
-
}),
|
|
322
|
-
);
|
|
323
|
-
},
|
|
324
|
-
|
|
325
|
-
async "chat.headers"(input: any, output: any): Promise<void> {
|
|
326
|
-
if (!flags.prompt_controls) return;
|
|
327
|
-
Object.assign(output.headers, {
|
|
328
|
-
"x-opencode-multiagent": "1",
|
|
329
|
-
"x-opencode-multiagent-profile": flags.profile ?? "standard",
|
|
330
|
-
"x-opencode-agent": label(input.agent) ?? "unknown",
|
|
331
|
-
});
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
async "chat.message"(input: any, output: any): Promise<void> {
|
|
335
|
-
if (!flags.prompt_controls || !risky(text(output.parts))) return;
|
|
336
|
-
await note(
|
|
337
|
-
"chat_risk",
|
|
338
|
-
compact({
|
|
339
|
-
observation: true,
|
|
340
|
-
sessionID: input.sessionID,
|
|
341
|
-
agent: label(input.agent),
|
|
342
|
-
text: clip(text(output.parts)),
|
|
343
|
-
}),
|
|
344
|
-
);
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
async "command.execute.before"(input: any): Promise<void> {
|
|
348
|
-
if (!flags.prompt_controls || !risky(input.arguments)) return;
|
|
349
|
-
await note(
|
|
350
|
-
"command_risk",
|
|
351
|
-
compact({
|
|
352
|
-
observation: true,
|
|
353
|
-
sessionID: input.sessionID,
|
|
354
|
-
command: input.command,
|
|
355
|
-
arguments: clip(input.arguments),
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
358
|
-
},
|
|
359
|
-
|
|
360
|
-
async "experimental.chat.system.transform"(_input: any, output: any): Promise<void> {
|
|
361
|
-
if (flags.experimental?.chat_system_transform !== true) return;
|
|
362
|
-
if (output.system.includes(experimentalText)) return;
|
|
363
|
-
output.system.push(experimentalText);
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
async "experimental.chat.messages.transform"(_input: any, output: any): Promise<void> {
|
|
367
|
-
if (flags.experimental?.chat_messages_transform !== true) return;
|
|
368
|
-
const last = [...output.messages].reverse().find((message: any) => message.info?.role === "user");
|
|
369
|
-
if (!last) return;
|
|
370
|
-
if (text(last.parts).includes(experimentalText)) return;
|
|
371
|
-
last.parts.push({ type: "text", text: experimentalText });
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
async "experimental.session.compacting"(_input: any, output: any): Promise<void> {
|
|
375
|
-
if (flags.experimental?.session_compacting !== true) return;
|
|
376
|
-
if (output.context.includes(experimentalText)) return;
|
|
377
|
-
output.context.push(experimentalText);
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
async "experimental.text.complete"(_input: any, output: any): Promise<void> {
|
|
381
|
-
if (flags.experimental?.text_complete !== true) return;
|
|
382
|
-
if (output.text.includes(experimentalText)) return;
|
|
383
|
-
output.text += `\n${experimentalText}`;
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
async config(sdkCfg: Config): Promise<void> {
|
|
387
|
-
// The SDK delivers Config; we treat it as our superset MergedConfig internally.
|
|
388
|
-
const cfg = sdkCfg as unknown as MergedConfig;
|
|
389
|
-
resolveExistingTierReferences(cfg, modelTiers);
|
|
390
|
-
if (flags.agent_compilation) {
|
|
391
|
-
mcpRegistry = await compileAgents(cfg, [bundledAgentsDir, globalAgentsDir, projectAgentsDir], modelTiers, agentSettings);
|
|
392
|
-
}
|
|
393
|
-
if (flags.command_compilation) {
|
|
394
|
-
await compileCommands(cfg, [bundledCommandsDir, globalCommandsDir, projectCommandsDir], modelTiers);
|
|
395
|
-
}
|
|
396
|
-
if (flags.mcp_compilation !== false) {
|
|
397
|
-
mcpDefaults ??= await loadMcpDefaults();
|
|
398
|
-
applyMcpDefaults(cfg, mcpDefaults);
|
|
399
|
-
}
|
|
400
|
-
resolveExistingTierReferences(cfg, modelTiers);
|
|
401
|
-
await applyBuiltInAgentPolicy(cfg);
|
|
402
|
-
if (flags.compiler?.permission_compilation === true) {
|
|
403
|
-
applyPermissionDefaults(cfg);
|
|
404
|
-
}
|
|
405
|
-
if (flags.skill_injection === true) {
|
|
406
|
-
skillRegistry ??= await loadSkillRegistry(flags.skill_sources, flags);
|
|
407
|
-
await note(
|
|
408
|
-
"skill_injection",
|
|
409
|
-
compact({
|
|
410
|
-
observation: true,
|
|
411
|
-
mode: "available",
|
|
412
|
-
skills: [...skillRegistry.keys()],
|
|
413
|
-
}),
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
async "tool.execute.before"(input: any, output: any): Promise<void> {
|
|
419
|
-
const agentInfo = sessionAgentMap.get(input.sessionID) ?? {};
|
|
420
|
-
const agentName = agentInfo.agent;
|
|
421
|
-
if (input.tool === "task") {
|
|
422
|
-
const targetAgent = getTaskTarget(output.args ?? input.args ?? {});
|
|
423
|
-
if (targetAgent === "qa") {
|
|
424
|
-
await trackQaDispatch(input.sessionID, agentInfo);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (input.tool === "webfetch" && typeof output.args?.url === "string" && output.args.url.startsWith("http://")) {
|
|
428
|
-
output.args.url = output.args.url.replace("http://", "https://");
|
|
429
|
-
}
|
|
430
|
-
if (flags.enforcement && ["read", "edit"].includes(input.tool) && typeof output.args?.filePath === "string" && blocked(output.args.filePath)) {
|
|
431
|
-
throw new Error("[opencode-multiagent] blocked sensitive path access");
|
|
432
|
-
}
|
|
433
|
-
if (flags.enforcement && input.tool === "bash" && typeof output.args?.command === "string" && tokenizedBashBlocked(output.args.command)) {
|
|
434
|
-
throw new Error("[opencode-multiagent] blocked destructive bash command");
|
|
435
|
-
}
|
|
436
|
-
if (input.tool === "edit" && typeof output.args?.filePath === "string") {
|
|
437
|
-
const lock = fileLocks.acquire(input.sessionID, output.args.filePath);
|
|
438
|
-
if (!lock.ok) {
|
|
439
|
-
telemetry.trackPermissionDenied(input.sessionID, { agent: agentName, model: agentInfo.model });
|
|
440
|
-
await note("permission_denied", {
|
|
441
|
-
observation: true,
|
|
442
|
-
sessionID: input.sessionID,
|
|
443
|
-
agent: agentName,
|
|
444
|
-
model: agentInfo.model,
|
|
445
|
-
tool: input.tool,
|
|
446
|
-
file: lock.filePath,
|
|
447
|
-
reason: "file_locked_by_other_session",
|
|
448
|
-
owner_sessionID: lock.ownerSessionID,
|
|
449
|
-
enforcement_layer: "plugin_hook",
|
|
450
|
-
});
|
|
451
|
-
throw new Error(`[opencode-multiagent] File lock conflict for ${lock.filePath}; active session ${lock.ownerSessionID}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (!flags.enforcement) return;
|
|
455
|
-
if (isMcpTool(input.tool) && agentName && mcpRegistry?.has(agentName)) {
|
|
456
|
-
const allowed = matchesMcpPermission(input.tool, mcpRegistry.get(agentName));
|
|
457
|
-
if (!allowed) {
|
|
458
|
-
telemetry.trackPermissionDenied(input.sessionID, { agent: agentName, model: agentInfo.model });
|
|
459
|
-
await note("permission_denied", {
|
|
460
|
-
observation: true,
|
|
461
|
-
sessionID: input.sessionID,
|
|
462
|
-
agent: agentName,
|
|
463
|
-
model: agentInfo.model,
|
|
464
|
-
tool: input.tool,
|
|
465
|
-
reason: "mcp_tool_not_allowed",
|
|
466
|
-
enforcement_layer: "plugin_hook",
|
|
467
|
-
});
|
|
468
|
-
throw new Error(`[opencode-multiagent] MCP tool ${input.tool} not allowed for agent ${agentName}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
|
|
473
|
-
async "tool.definition"(input: any, output: any): Promise<void> {
|
|
474
|
-
if (!flags.enforcement) return;
|
|
475
|
-
if (input.toolID === "bash") {
|
|
476
|
-
output.description +=
|
|
477
|
-
"\n\n[Policy] Commands matching destructive patterns (rm -rf /, sudo, shutdown, reboot, git reset --hard, etc.) are blocked by opencode-multiagent.";
|
|
478
|
-
}
|
|
479
|
-
if (input.toolID === "read") {
|
|
480
|
-
output.description +=
|
|
481
|
-
"\n\n[Policy] Reads targeting sensitive system paths (/etc, /root, /proc, /sys, /dev, /boot, .ssh) are blocked.";
|
|
482
|
-
}
|
|
483
|
-
if (input.toolID === "edit") {
|
|
484
|
-
output.description +=
|
|
485
|
-
"\n\n[Policy] Edits targeting sensitive system paths (/etc, /root, /proc, /sys, /dev, /boot, .ssh) are blocked.";
|
|
486
|
-
}
|
|
487
|
-
if (input.toolID === "webfetch") {
|
|
488
|
-
output.description += "\n\n[Policy] http:// URLs are automatically upgraded to https://.";
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
|
|
492
|
-
async "shell.env"(_input: any, output: any): Promise<void> {
|
|
493
|
-
if (!flags.enforcement) return;
|
|
494
|
-
Object.assign(output.env, {
|
|
495
|
-
OPENCODE_MULTIAGENT: "1",
|
|
496
|
-
OPENCODE_MULTIAGENT_MODE: pluginMode,
|
|
497
|
-
OPENCODE_MULTIAGENT_PROFILE: flags.profile ?? "standard",
|
|
498
|
-
OPENCODE_CONTROL_PLANE: "1",
|
|
499
|
-
});
|
|
500
|
-
},
|
|
501
|
-
|
|
502
|
-
async "tool.execute.after"(input: any, output: any): Promise<void> {
|
|
503
|
-
if (input.tool === "edit" && typeof input.args?.filePath === "string") {
|
|
504
|
-
quality.trackEdit(input.sessionID, input.args.filePath);
|
|
505
|
-
telemetry.trackEdit(input.sessionID);
|
|
506
|
-
fileLocks.acquire(input.sessionID, input.args.filePath);
|
|
507
|
-
}
|
|
508
|
-
if (input.tool === "bash" && typeof input.args?.command === "string") {
|
|
509
|
-
quality.recordQualityEvidence(input.sessionID, input.args.command);
|
|
510
|
-
}
|
|
511
|
-
const agentInfo = sessionAgentMap.get(input.sessionID) ?? {};
|
|
512
|
-
telemetry.trackToolCall(input.sessionID, { agent: agentInfo.agent, model: agentInfo.model });
|
|
513
|
-
if (typeof output.metadata?.exit === "number" && output.metadata.exit !== 0) {
|
|
514
|
-
telemetry.trackToolError(input.sessionID, { agent: agentInfo.agent, model: agentInfo.model });
|
|
515
|
-
await note("tool_error", {
|
|
516
|
-
observation: true,
|
|
517
|
-
sessionID: input.sessionID,
|
|
518
|
-
agent: agentInfo.agent,
|
|
519
|
-
model: agentInfo.model,
|
|
520
|
-
tool: input.tool,
|
|
521
|
-
callID: input.callID,
|
|
522
|
-
exit_code: output.metadata.exit,
|
|
523
|
-
error_type: "nonzero_exit",
|
|
524
|
-
title: clip(output.title),
|
|
525
|
-
output: clip(output.output),
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
if (!flags.observation) return;
|
|
529
|
-
const lower = `${String(output.title ?? "")}\n${String(output.output ?? "")}`.toLowerCase();
|
|
530
|
-
const marks = flagged(lower);
|
|
531
|
-
if (typeof output.metadata?.exit === "number" && output.metadata.exit !== 0) {
|
|
532
|
-
marks.push("nonzero_exit");
|
|
533
|
-
}
|
|
534
|
-
await note(
|
|
535
|
-
"tool_after",
|
|
536
|
-
compact({
|
|
537
|
-
observation: true,
|
|
538
|
-
tool: input.tool,
|
|
539
|
-
sessionID: input.sessionID,
|
|
540
|
-
callID: input.callID,
|
|
541
|
-
title: clip(output.title),
|
|
542
|
-
args: input.args,
|
|
543
|
-
suspicious: marks.length > 0,
|
|
544
|
-
flags: marks,
|
|
545
|
-
output: clip(output.output),
|
|
546
|
-
}),
|
|
547
|
-
);
|
|
548
|
-
},
|
|
549
|
-
|
|
550
|
-
tool: {
|
|
551
|
-
...createTeamTools(mailbox, teamManager, client, loadTeamProfiles),
|
|
552
|
-
|
|
553
|
-
task_create: pluginTool({
|
|
554
|
-
description: "Create a new task in the team task board",
|
|
555
|
-
args: {
|
|
556
|
-
title: pluginTool.schema.string().describe("Short title for the task"),
|
|
557
|
-
description: pluginTool.schema.string().describe("Detailed description of what needs to be done"),
|
|
558
|
-
assignedAgent: pluginTool.schema.string().optional().describe("Name of the agent to assign this task to"),
|
|
559
|
-
dependencies: pluginTool.schema
|
|
560
|
-
.array(pluginTool.schema.string())
|
|
561
|
-
.optional()
|
|
562
|
-
.describe("IDs of tasks this task depends on"),
|
|
563
|
-
priority: pluginTool.schema
|
|
564
|
-
.enum(["high", "medium", "low"])
|
|
565
|
-
.optional()
|
|
566
|
-
.describe("Task priority: high, medium, or low"),
|
|
567
|
-
},
|
|
568
|
-
async execute(args, ctx) {
|
|
569
|
-
return JSON.stringify(taskManager.create({ ...args, createdBy: ctx.agent }));
|
|
570
|
-
},
|
|
571
|
-
}),
|
|
572
|
-
|
|
573
|
-
task_update: pluginTool({
|
|
574
|
-
description: "Update a task's status or result on the team task board",
|
|
575
|
-
args: {
|
|
576
|
-
taskID: pluginTool.schema.string().describe("ID of the task to update"),
|
|
577
|
-
status: pluginTool.schema
|
|
578
|
-
.enum(["pending", "claimed", "in_progress", "completed", "failed", "blocked"])
|
|
579
|
-
.describe("New status for the task"),
|
|
580
|
-
result: pluginTool.schema.string().optional().describe("Result summary or notes"),
|
|
581
|
-
},
|
|
582
|
-
async execute(args) {
|
|
583
|
-
return JSON.stringify(taskManager.update(args.taskID, { status: args.status, result: args.result }));
|
|
584
|
-
},
|
|
585
|
-
}),
|
|
586
|
-
|
|
587
|
-
task_list: pluginTool({
|
|
588
|
-
description: "List tasks on the team task board with optional filters",
|
|
589
|
-
args: {
|
|
590
|
-
status: pluginTool.schema.string().optional().describe("Filter by task status"),
|
|
591
|
-
assignedAgent: pluginTool.schema.string().optional().describe("Filter by assigned agent name"),
|
|
592
|
-
},
|
|
593
|
-
async execute(args) {
|
|
594
|
-
return JSON.stringify(taskManager.list(args));
|
|
595
|
-
},
|
|
596
|
-
}),
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
import { logDirPath, logFilePath } from "./constants.ts";
|
|
4
|
-
|
|
5
|
-
export const note = async (kind: string, payload: Record<string, unknown>): Promise<void> => {
|
|
6
|
-
try {
|
|
7
|
-
await mkdir(logDirPath, { recursive: true });
|
|
8
|
-
await appendFile(logFilePath, `${JSON.stringify({ ts: Date.now(), kind, ...payload })}\n`);
|
|
9
|
-
} catch {
|
|
10
|
-
// Logging is best-effort.
|
|
11
|
-
}
|
|
12
|
-
};
|