@agentprojectcontext/apx 1.66.0 → 1.67.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/package.json +3 -2
- package/skills/apx/SKILL.md +3 -0
- package/src/core/agent/index.js +2 -0
- package/src/core/agent/judge.js +174 -0
- package/src/core/agent/model-router.js +107 -5
- package/src/core/agent/prompts/modes/code-build.md +1 -1
- package/src/core/agent/run-agent.js +149 -12
- package/src/core/agent/security.js +97 -0
- package/src/core/agent/stuck-detector.js +89 -0
- package/src/core/agent/super-agent.js +58 -17
- package/src/core/agent/tools/handlers/run-subagent.js +117 -0
- package/src/core/agent/tools/helpers.js +11 -1
- package/src/core/agent/tools/names.js +2 -0
- package/src/core/agent/tools/registry.js +10 -0
- package/src/core/artifacts/preview.js +392 -0
- package/src/core/artifacts/tunnel.js +169 -0
- package/src/core/config/index.js +61 -0
- package/src/core/config/secret-values.js +132 -0
- package/src/core/engines/mock.js +15 -1
- package/src/core/logging.js +10 -3
- package/src/core/memory/compactor.js +65 -56
- package/src/core/memory/summarizer.js +125 -0
- package/src/core/stores/conversations-compactor.js +24 -31
- package/src/host/daemon/api/admin-config.js +5 -0
- package/src/host/daemon/api/artifact-preview.js +82 -0
- package/src/host/daemon/api/web.js +1 -1
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/index.js +16 -1
- package/src/interfaces/acp/index.js +363 -0
- package/src/interfaces/acp/jsonrpc.js +180 -0
- package/src/interfaces/acp/session.js +205 -0
- package/src/interfaces/cli/commands/acp.js +10 -0
- package/src/interfaces/cli/commands/artifact.js +115 -0
- package/src/interfaces/cli/index.js +74 -0
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js +803 -0
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-BPGECxzm.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +6 -6
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +145 -2
- package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +236 -0
- package/src/interfaces/web/src/i18n/en.ts +47 -0
- package/src/interfaces/web/src/i18n/es.ts +47 -0
- package/src/interfaces/web/src/lib/api/artifacts.ts +38 -0
- package/src/interfaces/web/src/screens/base/ModelsTab.tsx +4 -2
- package/src/interfaces/web/src/types/daemon.ts +16 -0
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +0 -1
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js +0 -778
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Inline security-risk analysis (OpenHands LLMSecurityAnalyzer pattern).
|
|
2
|
+
//
|
|
3
|
+
// Instead of a second "is this safe?" LLM call, the model grades each of its
|
|
4
|
+
// OWN tool calls by filling a `security_risk` field injected into every tool
|
|
5
|
+
// schema. The agent loop extracts the field before execution and a
|
|
6
|
+
// confirmation policy decides whether the call pauses for human approval.
|
|
7
|
+
// Zero extra latency, zero extra tokens beyond the one enum argument.
|
|
8
|
+
//
|
|
9
|
+
// Risk semantics (ordered): LOW < MEDIUM < HIGH. UNKNOWN means the model
|
|
10
|
+
// omitted the field (weak models do) — the policy decides whether UNKNOWN
|
|
11
|
+
// pauses via `confirm_unknown`.
|
|
12
|
+
|
|
13
|
+
export const SECURITY_RISK_LEVELS = Object.freeze(["LOW", "MEDIUM", "HIGH"]);
|
|
14
|
+
export const SECURITY_RISK_UNKNOWN = "UNKNOWN";
|
|
15
|
+
|
|
16
|
+
const RISK_ORDER = { LOW: 1, MEDIUM: 2, HIGH: 3 };
|
|
17
|
+
|
|
18
|
+
// Loop-control / interaction tools that never touch the world — grading them
|
|
19
|
+
// only burns tokens and trains the model to rubber-stamp the field.
|
|
20
|
+
const RISK_EXEMPT_TOOLS = new Set(["finish", "ask_questions", "discover_tools"]);
|
|
21
|
+
|
|
22
|
+
export function normalizeRisk(value) {
|
|
23
|
+
const v = String(value || "").trim().toUpperCase();
|
|
24
|
+
return RISK_ORDER[v] ? v : SECURITY_RISK_UNKNOWN;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function securityRiskConfig(globalConfig) {
|
|
28
|
+
const raw = globalConfig?.super_agent?.security_risk || {};
|
|
29
|
+
return {
|
|
30
|
+
enabled: raw.enabled === true,
|
|
31
|
+
confirm_at: normalizeRisk(raw.confirm_at) === SECURITY_RISK_UNKNOWN ? "HIGH" : normalizeRisk(raw.confirm_at),
|
|
32
|
+
confirm_unknown: raw.confirm_unknown !== false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isSecurityRiskEnabled(globalConfig) {
|
|
37
|
+
return securityRiskConfig(globalConfig).enabled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SECURITY_RISK_PROPERTY = Object.freeze({
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: [...SECURITY_RISK_LEVELS],
|
|
43
|
+
description:
|
|
44
|
+
"Your assessment of the safety risk of this action. LOW: read-only or " +
|
|
45
|
+
"trivially reversible. MEDIUM: modifies local state but is reversible. " +
|
|
46
|
+
"HIGH: destructive, outward-facing (messages someone, publishes, spends) " +
|
|
47
|
+
"or hard to reverse.",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a copy of `schemas` where every eligible tool gains a required
|
|
52
|
+
* `security_risk` enum parameter. Originals are never mutated (schemas are
|
|
53
|
+
* module-level constants shared across sessions). The field is listed first
|
|
54
|
+
* so it stays visible to the model, mirroring OpenHands' field prioritization.
|
|
55
|
+
*/
|
|
56
|
+
export function withSecurityRiskField(schemas) {
|
|
57
|
+
return (schemas || []).map((s) => {
|
|
58
|
+
const fn = s?.function;
|
|
59
|
+
const name = fn?.name || s?.name;
|
|
60
|
+
if (!fn || !name || RISK_EXEMPT_TOOLS.has(name)) return s;
|
|
61
|
+
const params = fn.parameters || { type: "object", properties: {} };
|
|
62
|
+
if (params.properties?.security_risk) return s;
|
|
63
|
+
return {
|
|
64
|
+
...s,
|
|
65
|
+
function: {
|
|
66
|
+
...fn,
|
|
67
|
+
parameters: {
|
|
68
|
+
...params,
|
|
69
|
+
properties: { security_risk: SECURITY_RISK_PROPERTY, ...(params.properties || {}) },
|
|
70
|
+
required: ["security_risk", ...(params.required || [])],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract (and delete) `security_risk` from parsed tool args. Handlers never
|
|
79
|
+
* see the field — it belongs to the loop, not the tool contract.
|
|
80
|
+
*/
|
|
81
|
+
export function popSecurityRisk(args) {
|
|
82
|
+
if (!args || typeof args !== "object") return SECURITY_RISK_UNKNOWN;
|
|
83
|
+
const risk = normalizeRisk(args.security_risk);
|
|
84
|
+
delete args.security_risk;
|
|
85
|
+
return risk;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* ConfirmRisky policy: pause when the model's own grade meets the configured
|
|
90
|
+
* threshold (or when it didn't grade at all and confirm_unknown is on).
|
|
91
|
+
*/
|
|
92
|
+
export function shouldConfirmRisk(risk, cfg) {
|
|
93
|
+
if (!cfg?.enabled) return false;
|
|
94
|
+
const r = normalizeRisk(risk);
|
|
95
|
+
if (r === SECURITY_RISK_UNKNOWN) return cfg.confirm_unknown !== false;
|
|
96
|
+
return RISK_ORDER[r] >= RISK_ORDER[cfg.confirm_at || "HIGH"];
|
|
97
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Stuck detection (OpenHands StuckDetector pattern, adapted to the APX loop).
|
|
2
|
+
//
|
|
3
|
+
// Two loop shapes matter here — both invisible to the side-effect dedupe,
|
|
4
|
+
// which only guards mutating tools:
|
|
5
|
+
// - action_observation: the model re-issues the SAME call and gets the SAME
|
|
6
|
+
// result N times (read_file on the same path, list_* over and over);
|
|
7
|
+
// - action_error: the SAME call keeps erroring M times in a row (auth wall,
|
|
8
|
+
// missing binary) — different error text still counts, retrying is the
|
|
9
|
+
// stuck part.
|
|
10
|
+
//
|
|
11
|
+
// The loop's monologue/no-tool patterns don't apply: run-agent already breaks
|
|
12
|
+
// when a turn produces no tool calls.
|
|
13
|
+
|
|
14
|
+
export function stuckDetectionConfig(globalConfig) {
|
|
15
|
+
const raw = globalConfig?.super_agent?.stuck_detection || {};
|
|
16
|
+
return {
|
|
17
|
+
enabled: raw.enabled !== false,
|
|
18
|
+
action_repeat:
|
|
19
|
+
Number.isFinite(raw.action_repeat) && raw.action_repeat > 1 ? raw.action_repeat : 4,
|
|
20
|
+
error_repeat:
|
|
21
|
+
Number.isFinite(raw.error_repeat) && raw.error_repeat > 1 ? raw.error_repeat : 3,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WINDOW_SIZE = 20;
|
|
26
|
+
|
|
27
|
+
export function createStuckDetector(cfg) {
|
|
28
|
+
const window = [];
|
|
29
|
+
return {
|
|
30
|
+
record({ tool, argsSig, resultSig, isError }) {
|
|
31
|
+
window.push({ tool, argsSig, resultSig, isError: isError === true });
|
|
32
|
+
if (window.length > WINDOW_SIZE) window.shift();
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Returns { pattern, tool, repeats } when a stuck pattern closes on the
|
|
36
|
+
// latest record, else null. Checked after every tool execution.
|
|
37
|
+
check() {
|
|
38
|
+
if (!cfg.enabled) return null;
|
|
39
|
+
|
|
40
|
+
const errs = window.slice(-cfg.error_repeat);
|
|
41
|
+
if (
|
|
42
|
+
errs.length === cfg.error_repeat &&
|
|
43
|
+
errs.every(
|
|
44
|
+
(r) => r.isError && r.tool === errs[0].tool && r.argsSig === errs[0].argsSig
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
return { pattern: "action_error", tool: errs[0].tool, repeats: cfg.error_repeat };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const acts = window.slice(-cfg.action_repeat);
|
|
51
|
+
if (
|
|
52
|
+
acts.length === cfg.action_repeat &&
|
|
53
|
+
acts.every(
|
|
54
|
+
(r) =>
|
|
55
|
+
r.tool === acts[0].tool &&
|
|
56
|
+
r.argsSig === acts[0].argsSig &&
|
|
57
|
+
r.resultSig === acts[0].resultSig
|
|
58
|
+
)
|
|
59
|
+
) {
|
|
60
|
+
return { pattern: "action_observation", tool: acts[0].tool, repeats: cfg.action_repeat };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Cleared after a nudge so old records can't instantly re-trigger — only
|
|
67
|
+
// NEW repetitions after the warning count towards the abort.
|
|
68
|
+
reset() {
|
|
69
|
+
window.length = 0;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// In-band note, WRAPUP_SIGNAL-style: shapes behavior only, wording stays the
|
|
75
|
+
// model's. Delivered as a conversation turn because weak models reliably
|
|
76
|
+
// answer the latest turn but routinely ignore system-suffix nudges.
|
|
77
|
+
export function stuckNudgeSignal({ tool, repeats, pattern }) {
|
|
78
|
+
const shape =
|
|
79
|
+
pattern === "action_error"
|
|
80
|
+
? `kept failing the same way ${repeats} times in a row`
|
|
81
|
+
: `returned the same result ${repeats} times in a row`;
|
|
82
|
+
return (
|
|
83
|
+
`[Internal turn note — this is NOT from the user. Your call to \`${tool}\` ` +
|
|
84
|
+
`${shape}. You appear to be stuck in a loop. Do NOT repeat that exact call ` +
|
|
85
|
+
"again. Either take a genuinely different approach (different tool, " +
|
|
86
|
+
"different arguments), or — if you cannot advance without help — tell the " +
|
|
87
|
+
"user plainly, in their language, what you tried and what is blocking you.]"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
isSuperAgentEnabled,
|
|
9
9
|
buildIdentityBlock,
|
|
10
10
|
loadDefaultSystemPrompt,
|
|
11
|
+
selectModelByRules,
|
|
11
12
|
} from "#core/agent/index.js";
|
|
12
13
|
import { resolveAgentName } from "#core/identity/index.js";
|
|
13
|
-
import { memoryBlockFor } from "#core/memory/index.js";
|
|
14
|
+
import { memoryBlockFor, buildActiveThreadsBlock } from "#core/memory/index.js";
|
|
14
15
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
16
|
+
import { judgeConfig, judgeCompletion, applyJudgeLoop } from "#core/agent/judge.js";
|
|
15
17
|
|
|
16
18
|
export {
|
|
17
19
|
buildIdentityBlock,
|
|
@@ -70,6 +72,9 @@ export async function runSuperAgent({
|
|
|
70
72
|
// because a per-turn skill inspector already injected the right context.
|
|
71
73
|
// Set by the daemon's super-agent endpoint when config.skills.inspector is on.
|
|
72
74
|
skipSkillsHint = false,
|
|
75
|
+
// Nesting depth of this run. 0 = user-facing turn; the run_subagent tool
|
|
76
|
+
// spawns children with depth+1 and refuses past its MAX_DEPTH.
|
|
77
|
+
subagentDepth = 0,
|
|
73
78
|
}) {
|
|
74
79
|
if (!isSuperAgentEnabled(globalConfig)) {
|
|
75
80
|
throw new Error("super-agent not enabled (set super_agent.enabled and .model in ~/.apx/config.json)");
|
|
@@ -129,22 +134,58 @@ export async function runSuperAgent({
|
|
|
129
134
|
|
|
130
135
|
const toolSchemas = noTools ? [] : toolSession.initialSchemas;
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// Content-based routing (RouterLLM pattern): rules in super_agent.routing
|
|
138
|
+
// inspect THIS turn (images, size, channel, keywords) and prefer a model for
|
|
139
|
+
// it. An explicit overrideModel always wins; the preferred model is
|
|
140
|
+
// health-checked in runAgent and falls back down the regular chain.
|
|
141
|
+
const contentRoute = overrideModel
|
|
142
|
+
? null
|
|
143
|
+
: selectModelByRules({ prompt, previousMessages, channel, channelMeta }, globalConfig);
|
|
144
|
+
|
|
145
|
+
const runOnce = (turnPrompt, history) =>
|
|
146
|
+
runAgent({
|
|
147
|
+
globalConfig,
|
|
148
|
+
system,
|
|
149
|
+
prompt: turnPrompt,
|
|
150
|
+
previousMessages: history,
|
|
151
|
+
overrideModel,
|
|
152
|
+
preferredModel: contentRoute?.model || null,
|
|
153
|
+
toolSchemas,
|
|
154
|
+
makeToolHandlers,
|
|
155
|
+
toolHandlerCtx: { projects, plugins, registries, globalConfig, channel, channelMeta, toolSession, requestConfirmation, backgroundResultSink, subagentDepth },
|
|
156
|
+
onEvent,
|
|
157
|
+
signal,
|
|
158
|
+
onToken,
|
|
159
|
+
agentName: resolveAgentName(globalConfig),
|
|
160
|
+
suppressTools,
|
|
161
|
+
...(maxTokens ? { maxTokens } : {}),
|
|
162
|
+
...(maxIters ? { maxIters } : {}),
|
|
163
|
+
...(completionContract ? { completionContract: true } : {}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await runOnce(prompt, previousMessages);
|
|
167
|
+
|
|
168
|
+
// Goal-completion judge (OpenHands critic pattern): opt-in, and only where
|
|
169
|
+
// "done" is a checkable claim — completion-contract turns at the top level.
|
|
170
|
+
// Sub-agent runs are excluded (the parent already judges the overall turn);
|
|
171
|
+
// tool-free callers (summarize/ask) have nothing to verify.
|
|
172
|
+
const jCfg = judgeConfig(globalConfig);
|
|
173
|
+
if (!jCfg.enabled || !completionContract || noTools || subagentDepth > 0) {
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
// Rolling refinement history: each round sees the original goal, its own
|
|
177
|
+
// prior reply, and the judge's follow-up as ordinary conversation turns.
|
|
178
|
+
const history = [...previousMessages, { role: "user", content: prompt }];
|
|
179
|
+
return applyJudgeLoop({
|
|
180
|
+
initialResult: result,
|
|
181
|
+
cfg: jCfg,
|
|
141
182
|
onEvent,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
judgeFn: (r) => judgeCompletion({ goal: prompt, result: r, globalConfig }),
|
|
184
|
+
runFollowup: async (followup, prior) => {
|
|
185
|
+
history.push({ role: "assistant", content: prior.text || "" });
|
|
186
|
+
const next = await runOnce(followup, [...history]);
|
|
187
|
+
history.push({ role: "user", content: followup });
|
|
188
|
+
return next;
|
|
189
|
+
},
|
|
149
190
|
});
|
|
150
191
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Sub-agents as a composable tool (OpenHands Task-tool pattern): the model
|
|
2
|
+
// delegates a self-contained chunk of work to an ISOLATED agent run — fresh
|
|
3
|
+
// conversation, same registry (minus user-interaction tools), runs to
|
|
4
|
+
// completion, and the final text comes back as this tool's observation.
|
|
5
|
+
import { TOOLS } from "../names.js";
|
|
6
|
+
|
|
7
|
+
// One level of nesting only: a sub-agent cannot spawn another sub-agent.
|
|
8
|
+
// Enough for delegation, and it hard-bounds the fan-out a confused model can
|
|
9
|
+
// create (depth 2 with a loop would be exponential).
|
|
10
|
+
const MAX_DEPTH = 1;
|
|
11
|
+
const DEFAULT_MAX_ITERS = 16;
|
|
12
|
+
const MAX_MAX_ITERS = 24;
|
|
13
|
+
|
|
14
|
+
// Withheld from the child: it has no path to the user (the parent relays), so
|
|
15
|
+
// user-interaction tools would either dead-end (ask_questions) or bypass the
|
|
16
|
+
// parent's narrative (send_telegram). run_subagent itself enforces MAX_DEPTH
|
|
17
|
+
// twice — suppression here, depth check in the handler.
|
|
18
|
+
const CHILD_SUPPRESSED_TOOLS = [
|
|
19
|
+
TOOLS.RUN_SUBAGENT,
|
|
20
|
+
TOOLS.ASK_QUESTIONS,
|
|
21
|
+
TOOLS.SEND_TELEGRAM,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
name: "run_subagent",
|
|
26
|
+
|
|
27
|
+
schema: {
|
|
28
|
+
type: "function",
|
|
29
|
+
function: {
|
|
30
|
+
name: "run_subagent",
|
|
31
|
+
description:
|
|
32
|
+
"Spawn an isolated sub-agent for a self-contained task and return its " +
|
|
33
|
+
"result. The sub-agent starts with a FRESH context (it does not see " +
|
|
34
|
+
"this conversation), gets the regular tool registry minus " +
|
|
35
|
+
"user-interaction tools, works until done and hands back its final " +
|
|
36
|
+
"answer. Use it to delegate a bounded chunk of work (research a " +
|
|
37
|
+
"question, refactor a file, produce a summary) while you keep the main " +
|
|
38
|
+
"thread. The prompt MUST be self-contained: include file paths, " +
|
|
39
|
+
"constraints and every piece of context the sub-agent needs.",
|
|
40
|
+
parameters: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
prompt: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Full, self-contained task for the sub-agent.",
|
|
46
|
+
},
|
|
47
|
+
description: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Short (3-5 word) label for the task.",
|
|
50
|
+
},
|
|
51
|
+
max_iters: {
|
|
52
|
+
type: "integer",
|
|
53
|
+
description: `Optional tool-step budget (default ${DEFAULT_MAX_ITERS}, max ${MAX_MAX_ITERS}).`,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["prompt"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
makeHandler: (ctx) => async ({ prompt, description, max_iters } = {}) => {
|
|
62
|
+
if (!prompt || !String(prompt).trim()) {
|
|
63
|
+
return { error: "run_subagent: prompt is required" };
|
|
64
|
+
}
|
|
65
|
+
const depth = Number(ctx.subagentDepth) || 0;
|
|
66
|
+
if (depth >= MAX_DEPTH) {
|
|
67
|
+
return {
|
|
68
|
+
error:
|
|
69
|
+
"run_subagent: nesting limit reached — a sub-agent cannot spawn another sub-agent. Do the work directly.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Dynamic import: registry → this handler → super-agent → registry would
|
|
74
|
+
// otherwise be a static ESM cycle.
|
|
75
|
+
const { runSuperAgent } = await import("#core/agent/super-agent.js");
|
|
76
|
+
|
|
77
|
+
const parsed = parseInt(max_iters, 10);
|
|
78
|
+
const maxIters = Math.min(
|
|
79
|
+
Math.max(Number.isFinite(parsed) ? parsed : DEFAULT_MAX_ITERS, 2),
|
|
80
|
+
MAX_MAX_ITERS
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const started = Date.now();
|
|
84
|
+
try {
|
|
85
|
+
const result = await runSuperAgent({
|
|
86
|
+
globalConfig: ctx.globalConfig,
|
|
87
|
+
projects: ctx.projects,
|
|
88
|
+
plugins: ctx.plugins,
|
|
89
|
+
registries: ctx.registries,
|
|
90
|
+
prompt: String(prompt),
|
|
91
|
+
channel: ctx.channel,
|
|
92
|
+
channelMeta: ctx.channelMeta,
|
|
93
|
+
previousMessages: [],
|
|
94
|
+
// Confirmations still reach the human: the child inherits the parent's
|
|
95
|
+
// channel adapter, so a risky child action pauses the same dialog.
|
|
96
|
+
requestConfirmation: ctx.requestConfirmation || null,
|
|
97
|
+
suppressTools: CHILD_SUPPRESSED_TOOLS,
|
|
98
|
+
maxIters,
|
|
99
|
+
// Delegation semantics: the child works until it declares done via
|
|
100
|
+
// `finish` — it can't end its run by narrating the next step.
|
|
101
|
+
completionContract: true,
|
|
102
|
+
subagentDepth: depth + 1,
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
...(description ? { description } : {}),
|
|
107
|
+
text: result.text || "",
|
|
108
|
+
steps: Array.isArray(result.trace) ? result.trace.length : 0,
|
|
109
|
+
model: result.model,
|
|
110
|
+
usage: result.usage,
|
|
111
|
+
duration_ms: Date.now() - started,
|
|
112
|
+
};
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { error: `run_subagent failed: ${e.message}` };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -38,6 +38,13 @@ export function buildAgentSystem(project, agent, opts = {}) {
|
|
|
38
38
|
|
|
39
39
|
export function createPermissionGuard(globalConfig = {}, {
|
|
40
40
|
requestConfirmation = null,
|
|
41
|
+
// Inline security-risk gate handshake (see run-agent.js). A callable that
|
|
42
|
+
// returns `{active, cleared}` for the tool call currently executing:
|
|
43
|
+
// active — the risk analyzer owns dangerous-call gating this session, so
|
|
44
|
+
// the static dangerous-flag branch of "automatico" stands down.
|
|
45
|
+
// cleared — the user already approved THIS call at the risk gate; asking
|
|
46
|
+
// again here would double-prompt.
|
|
47
|
+
securityGate = null,
|
|
41
48
|
} = {}) {
|
|
42
49
|
const permissionMode = globalConfig.super_agent?.permission_mode || DEFAULT_PERMISSION_MODE;
|
|
43
50
|
const allowedTools = new Set(globalConfig.super_agent?.allowed_tools || []);
|
|
@@ -49,9 +56,12 @@ export function createPermissionGuard(globalConfig = {}, {
|
|
|
49
56
|
return async function requirePermission(tool, { dangerous = false, args } = {}) {
|
|
50
57
|
if (permissionMode === PERMISSION_MODES.TOTAL) return;
|
|
51
58
|
|
|
59
|
+
const gate = typeof securityGate === "function" ? securityGate() : null;
|
|
60
|
+
if (gate?.cleared) return;
|
|
61
|
+
|
|
52
62
|
const blocked =
|
|
53
63
|
(permissionMode === PERMISSION_MODES.PERMISO && !allowedTools.has(tool)) ||
|
|
54
|
-
(permissionMode === PERMISSION_MODES.AUTOMATICO && dangerous);
|
|
64
|
+
(permissionMode === PERMISSION_MODES.AUTOMATICO && dangerous && !gate?.active);
|
|
55
65
|
|
|
56
66
|
if (!blocked) return;
|
|
57
67
|
|
|
@@ -49,6 +49,7 @@ export const TOOLS = Object.freeze({
|
|
|
49
49
|
CALL_AGENT: "call_agent",
|
|
50
50
|
CALL_MCP: "call_mcp",
|
|
51
51
|
CALL_RUNTIME: "call_runtime",
|
|
52
|
+
RUN_SUBAGENT: "run_subagent",
|
|
52
53
|
|
|
53
54
|
// Integrations — Asana plugin (see core/integrations/plugins/asana.js)
|
|
54
55
|
ASANA_LIST_PROJECTS: "asana_list_projects",
|
|
@@ -104,6 +105,7 @@ export const NATIVE_TOOL_NAMES = new Set([
|
|
|
104
105
|
TOOLS.CALL_AGENT,
|
|
105
106
|
TOOLS.CALL_MCP,
|
|
106
107
|
TOOLS.CALL_RUNTIME,
|
|
108
|
+
TOOLS.RUN_SUBAGENT,
|
|
107
109
|
TOOLS.ASANA_LIST_PROJECTS,
|
|
108
110
|
TOOLS.ASANA_LIST_TASKS,
|
|
109
111
|
TOOLS.ASANA_CREATE_TASK,
|
|
@@ -18,6 +18,7 @@ import searchSessions from "./handlers/search-sessions.js";
|
|
|
18
18
|
import callAgent from "./handlers/call-agent.js";
|
|
19
19
|
import callMcp from "./handlers/call-mcp.js";
|
|
20
20
|
import callRuntime from "./handlers/call-runtime.js";
|
|
21
|
+
import runSubagent from "./handlers/run-subagent.js";
|
|
21
22
|
import sendTelegram from "./handlers/send-telegram.js";
|
|
22
23
|
import setIdentity from "./handlers/set-identity.js";
|
|
23
24
|
import setPermissionMode from "./handlers/set-permission-mode.js";
|
|
@@ -65,6 +66,7 @@ const NATIVE_TOOLS = [
|
|
|
65
66
|
callAgent,
|
|
66
67
|
callMcp,
|
|
67
68
|
callRuntime,
|
|
69
|
+
runSubagent,
|
|
68
70
|
sendTelegram,
|
|
69
71
|
setIdentity,
|
|
70
72
|
setPermissionMode,
|
|
@@ -184,6 +186,7 @@ const NATIVE_CATEGORY = {
|
|
|
184
186
|
[TOOLS.IMPORT_AGENT]: "agents",
|
|
185
187
|
[TOOLS.ADD_PROJECT]: "projects",
|
|
186
188
|
[TOOLS.CALL_AGENT]: "agents",
|
|
189
|
+
[TOOLS.RUN_SUBAGENT]: "agents",
|
|
187
190
|
[TOOLS.CALL_RUNTIME]: "runtime",
|
|
188
191
|
[TOOLS.CALL_MCP]: "mcp",
|
|
189
192
|
[TOOLS.READ_AGENT_MEMORY]: "memory",
|
|
@@ -384,6 +387,13 @@ export function makeToolHandlers(ctx) {
|
|
|
384
387
|
...ctx,
|
|
385
388
|
requirePermission: createPermissionGuard(ctx.globalConfig || {}, {
|
|
386
389
|
requestConfirmation: ctx.requestConfirmation || null,
|
|
390
|
+
// Live view of the risk-gate state for the CURRENT tool call. Reads the
|
|
391
|
+
// original ctx object (not the spread copy) because run-agent.js mutates
|
|
392
|
+
// it per call — see the security-risk section of the loop.
|
|
393
|
+
securityGate: () => ({
|
|
394
|
+
active: ctx.securityRiskActive === true,
|
|
395
|
+
cleared: ctx.securityGateCleared === true,
|
|
396
|
+
}),
|
|
387
397
|
}),
|
|
388
398
|
};
|
|
389
399
|
return Object.fromEntries(ALL_TOOLS.map((tool) => [tool.name, tool.makeHandler(toolCtx)]));
|