@gajae-code/coding-agent 0.11.9 → 0.11.11
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/CHANGELOG.md +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -35,17 +35,28 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
38
|
+
/**
|
|
39
|
+
* Only `/dev/null` is exempt. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>` are descriptor
|
|
40
|
+
* aliases: `exec 1<>src/product.ts; printf x >/dev/stdout` reaches a real repository file
|
|
41
|
+
* through a rebound descriptor, so they must stay blocked.
|
|
42
|
+
*/
|
|
43
|
+
const DEVICE_SINK_PATHS = new Set(["/dev/null"]);
|
|
44
|
+
/** Bash write forms the plain `>`/`>>` scanner misses: clobber, dup-to-path, and read-write open. */
|
|
45
|
+
const BASH_EXTENDED_WRITE_RE = /(?:>\||<>|>&(?![\s;&|]*(?:\d+|-)(?:[\s;&|]|$)))\s*([^\s;&|]+)/g;
|
|
46
|
+
/** Descriptor rebinding is not statically resolvable; any `exec` redirection forces a fail-closed verdict. */
|
|
47
|
+
const BASH_EXEC_REDIRECT_RE = /(?:^|[;&|\n])\s*(?:sudo\s+)?exec\b[^;&|\n]*[<>]/i;
|
|
48
|
+
|
|
38
49
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
39
50
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
40
51
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
41
52
|
const BASH_MUTATION_COMMAND_RE =
|
|
42
|
-
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
53
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
43
54
|
const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
|
|
44
55
|
const BASH_OPAQUE_INTERPRETER_WRITE_RE =
|
|
45
56
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
46
57
|
const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
|
|
47
58
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
48
|
-
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
|
|
59
|
+
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?dd\b([^;&|\n]*)/gi;
|
|
49
60
|
|
|
50
61
|
type ToolWithEditMode = AgentTool & {
|
|
51
62
|
mode?: unknown;
|
|
@@ -371,6 +382,10 @@ function cleanShellWord(value: string): string {
|
|
|
371
382
|
return value.replace(/^['"]|['"]$/g, "");
|
|
372
383
|
}
|
|
373
384
|
|
|
385
|
+
function isDeviceSinkPath(value: string): boolean {
|
|
386
|
+
return DEVICE_SINK_PATHS.has(cleanShellWord(value));
|
|
387
|
+
}
|
|
388
|
+
|
|
374
389
|
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
375
390
|
const record = getRecord(args);
|
|
376
391
|
const command = safeString(record?.command);
|
|
@@ -382,11 +397,25 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
382
397
|
if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
|
|
383
398
|
targets.unknown = true;
|
|
384
399
|
}
|
|
400
|
+
// `exec 1<>file` rebinds a descriptor, so a later `>/dev/null` may not reach the device at all.
|
|
401
|
+
if (BASH_EXEC_REDIRECT_RE.test(command)) {
|
|
402
|
+
targets.unknown = true;
|
|
403
|
+
}
|
|
404
|
+
for (const match of command.matchAll(BASH_EXTENDED_WRITE_RE)) {
|
|
405
|
+
const cleaned = cleanShellWord(match[1] ?? "");
|
|
406
|
+
if (!cleaned) targets.unknown = true;
|
|
407
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
408
|
+
}
|
|
385
409
|
for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
|
|
386
410
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
411
|
+
// Every `of=` counts: GNU dd honors the last one, so `of=/dev/null of=real.ts` writes real.ts.
|
|
412
|
+
const outputs = parts.filter(part => part.startsWith("of="));
|
|
413
|
+
if (outputs.length === 0) targets.unknown = true;
|
|
414
|
+
for (const output of outputs) {
|
|
415
|
+
const outputPath = cleanShellWord(output.slice(3));
|
|
416
|
+
if (!outputPath) targets.unknown = true;
|
|
417
|
+
else if (!isDeviceSinkPath(outputPath)) addPath(targets, outputPath);
|
|
418
|
+
}
|
|
390
419
|
}
|
|
391
420
|
for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
|
|
392
421
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
@@ -399,12 +428,17 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
399
428
|
for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
|
|
400
429
|
const redirected = match[2]?.trim();
|
|
401
430
|
if (redirected) {
|
|
402
|
-
|
|
431
|
+
const cleaned = cleanShellWord(redirected);
|
|
432
|
+
// A capture that dequotes to nothing (e.g. `>" src.ts"`) is a truncated parse, not a safe no-op.
|
|
433
|
+
if (!cleaned) targets.unknown = true;
|
|
434
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
403
435
|
continue;
|
|
404
436
|
}
|
|
405
437
|
const parts = shellWords(match[1] ?? "");
|
|
406
438
|
const commandName = match[0]
|
|
407
|
-
?.match(
|
|
439
|
+
?.match(
|
|
440
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i,
|
|
441
|
+
)?.[1]
|
|
408
442
|
?.toLowerCase();
|
|
409
443
|
const targetParts =
|
|
410
444
|
commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
|
package/src/task/executor.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
} from "@gajae-code/agent-core";
|
|
16
16
|
import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
|
|
17
17
|
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
18
|
-
import type { Message, Model, ServiceTier } from "@gajae-code/ai";
|
|
18
|
+
import type { AssistantMessage, Message, Model, ServiceTier } from "@gajae-code/ai";
|
|
19
19
|
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
|
|
20
20
|
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
21
21
|
import { AsyncJobManager } from "../async";
|
|
@@ -50,6 +50,7 @@ import type { EventBus } from "../utils/event-bus";
|
|
|
50
50
|
import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
51
51
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
52
52
|
import { validateAllocatedTaskId } from "./id";
|
|
53
|
+
import { classifyProviderRetry, providerNameFromModel } from "./provider-retry-status";
|
|
53
54
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
54
55
|
import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
55
56
|
import {
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
67
68
|
type TaskToolDetails,
|
|
68
69
|
} from "./types";
|
|
70
|
+
import { type ExecutorExecutionMode, resolveUltragoalRedTeamActivation } from "./ultragoal-redteam-activation";
|
|
69
71
|
|
|
70
72
|
/** Agent event types to forward for progress tracking. */
|
|
71
73
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
@@ -81,6 +83,21 @@ const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
|
81
83
|
"tool_execution_end",
|
|
82
84
|
]);
|
|
83
85
|
|
|
86
|
+
const providerStreamingUpdateTypes = new Set<string>([
|
|
87
|
+
"text_start",
|
|
88
|
+
"text_delta",
|
|
89
|
+
"text_end",
|
|
90
|
+
"thinking_start",
|
|
91
|
+
"thinking_delta",
|
|
92
|
+
"thinking_end",
|
|
93
|
+
"reasoning_summary_start",
|
|
94
|
+
"reasoning_summary_delta",
|
|
95
|
+
"reasoning_summary_end",
|
|
96
|
+
"toolcall_start",
|
|
97
|
+
"toolcall_delta",
|
|
98
|
+
"toolcall_end",
|
|
99
|
+
]);
|
|
100
|
+
|
|
84
101
|
const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent =>
|
|
85
102
|
agentEventTypes.has(event.type as AgentEvent["type"]);
|
|
86
103
|
|
|
@@ -174,6 +191,11 @@ export interface ExecutorOptions {
|
|
|
174
191
|
agent: AgentDefinition;
|
|
175
192
|
task: string;
|
|
176
193
|
assignment?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Typed executor execution mode. When set, overrides assignment-text heuristics
|
|
196
|
+
* for ultragoal red-team prompt injection (#2698 / #2456).
|
|
197
|
+
*/
|
|
198
|
+
executionMode?: ExecutorExecutionMode;
|
|
177
199
|
context?: string;
|
|
178
200
|
description?: string;
|
|
179
201
|
index: number;
|
|
@@ -845,7 +867,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
845
867
|
let modelSubstitutionWarning: ModelSubstitutionWarning | undefined;
|
|
846
868
|
let resolvedModelString: string | undefined;
|
|
847
869
|
let lastAssistantModelString: string | undefined;
|
|
870
|
+
let activeProviderModelString: string | undefined;
|
|
848
871
|
let effectiveThinkingLevelForWarning: ThinkingLevel | undefined;
|
|
872
|
+
let lastProviderProgressAtMs: number | undefined;
|
|
873
|
+
let sessionEventOrdinal = 0;
|
|
874
|
+
let retryStartOrdinal = 0;
|
|
875
|
+
const seenAssistantMessages = new WeakSet<AgentMessage>();
|
|
876
|
+
const seenAssistantMessageIdentities = new Set<string>();
|
|
849
877
|
|
|
850
878
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
851
879
|
const accumulatedUsage = {
|
|
@@ -927,7 +955,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
927
955
|
|
|
928
956
|
const emitProgressNow = () => {
|
|
929
957
|
progress.durationMs = Date.now() - startTime;
|
|
930
|
-
|
|
958
|
+
const progressSnapshot = structuredClone(progress);
|
|
959
|
+
onProgress?.(progressSnapshot);
|
|
931
960
|
if (options.eventBus) {
|
|
932
961
|
options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
|
|
933
962
|
index,
|
|
@@ -935,7 +964,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
935
964
|
agentSource: agent.source,
|
|
936
965
|
task,
|
|
937
966
|
assignment,
|
|
938
|
-
progress:
|
|
967
|
+
progress: progressSnapshot,
|
|
939
968
|
sessionFile: null,
|
|
940
969
|
});
|
|
941
970
|
}
|
|
@@ -990,6 +1019,79 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
990
1019
|
: undefined;
|
|
991
1020
|
};
|
|
992
1021
|
|
|
1022
|
+
const getAssistantMessageIdentity = (message: AgentMessage): string | undefined => {
|
|
1023
|
+
if (message.role !== "assistant") return undefined;
|
|
1024
|
+
const model = getMessageModelString(message);
|
|
1025
|
+
const timestamp = "timestamp" in message ? message.timestamp : undefined;
|
|
1026
|
+
if (!model || typeof timestamp !== "number") return undefined;
|
|
1027
|
+
const responseId = "responseId" in message && typeof message.responseId === "string" ? message.responseId : "";
|
|
1028
|
+
return JSON.stringify([model, timestamp, responseId]);
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const rememberAssistantMessage = (message: AgentMessage): void => {
|
|
1032
|
+
seenAssistantMessages.add(message);
|
|
1033
|
+
const identity = getAssistantMessageIdentity(message);
|
|
1034
|
+
if (identity) seenAssistantMessageIdentities.add(identity);
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const isProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
|
|
1038
|
+
message.role === "assistant" &&
|
|
1039
|
+
"stopReason" in message &&
|
|
1040
|
+
Array.isArray(message.content) &&
|
|
1041
|
+
getMessageModelString(message) !== undefined;
|
|
1042
|
+
|
|
1043
|
+
const isSuccessfulProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
|
|
1044
|
+
isProviderAssistantMessage(message) &&
|
|
1045
|
+
message.stopReason !== "error" &&
|
|
1046
|
+
message.stopReason !== "aborted" &&
|
|
1047
|
+
message.errorMessage === undefined &&
|
|
1048
|
+
message.errorKind === undefined &&
|
|
1049
|
+
message.transportFailure === undefined;
|
|
1050
|
+
|
|
1051
|
+
const hasMatchingActiveProviderModel = (message: AgentMessage): message is AssistantMessage =>
|
|
1052
|
+
isProviderAssistantMessage(message) &&
|
|
1053
|
+
activeProviderModelString !== undefined &&
|
|
1054
|
+
getMessageModelString(message) === activeProviderModelString;
|
|
1055
|
+
|
|
1056
|
+
const isProviderStreamingUpdate = (event: Extract<AgentEvent, { type: "message_update" }>): boolean => {
|
|
1057
|
+
const update = event.assistantMessageEvent;
|
|
1058
|
+
if (!update || typeof update !== "object" || !providerStreamingUpdateTypes.has(update.type)) return false;
|
|
1059
|
+
if (!("partial" in update) || getMessageModelString(update.partial) !== activeProviderModelString) return false;
|
|
1060
|
+
switch (update.type) {
|
|
1061
|
+
case "text_delta":
|
|
1062
|
+
case "thinking_delta":
|
|
1063
|
+
case "reasoning_summary_delta":
|
|
1064
|
+
case "toolcall_delta":
|
|
1065
|
+
return typeof update.delta === "string";
|
|
1066
|
+
default:
|
|
1067
|
+
return typeof update.contentIndex === "number";
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
const getProviderTextDelta = (event: Extract<AgentEvent, { type: "message_update" }>): string | undefined => {
|
|
1072
|
+
const update = (event as { assistantMessageEvent?: unknown }).assistantMessageEvent;
|
|
1073
|
+
if (!update || typeof update !== "object") return undefined;
|
|
1074
|
+
const record = update as { type?: unknown; delta?: unknown };
|
|
1075
|
+
return record.type === "text_delta" && typeof record.delta === "string" ? record.delta : undefined;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const isProviderBackedRecoveryEvent = (
|
|
1079
|
+
event: Extract<AgentEvent, { type: "message_start" | "message_update" }>,
|
|
1080
|
+
eventOrdinal: number,
|
|
1081
|
+
): boolean => {
|
|
1082
|
+
const messageIdentity = getAssistantMessageIdentity(event.message);
|
|
1083
|
+
if (
|
|
1084
|
+
!progress.retryState ||
|
|
1085
|
+
eventOrdinal <= retryStartOrdinal ||
|
|
1086
|
+
seenAssistantMessages.has(event.message) ||
|
|
1087
|
+
(messageIdentity !== undefined && seenAssistantMessageIdentities.has(messageIdentity))
|
|
1088
|
+
)
|
|
1089
|
+
return false;
|
|
1090
|
+
if (!hasMatchingActiveProviderModel(event.message)) return false;
|
|
1091
|
+
if (event.type === "message_start") return isSuccessfulProviderAssistantMessage(event.message);
|
|
1092
|
+
return isProviderStreamingUpdate(event);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
993
1095
|
const updateRecentOutputLines = () => {
|
|
994
1096
|
const lines = recentOutputTail.split("\n").filter(line => line.trim());
|
|
995
1097
|
progress.recentOutput = lines.slice(-8).reverse();
|
|
@@ -1004,21 +1106,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1004
1106
|
updateRecentOutputLines();
|
|
1005
1107
|
};
|
|
1006
1108
|
|
|
1007
|
-
const replaceRecentOutputFromContent = (content: unknown[]) => {
|
|
1008
|
-
recentOutputTail = "";
|
|
1009
|
-
for (const block of content) {
|
|
1010
|
-
if (!block || typeof block !== "object") continue;
|
|
1011
|
-
const record = block as { type?: unknown; text?: unknown };
|
|
1012
|
-
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
1013
|
-
if (!record.text) continue;
|
|
1014
|
-
recentOutputTail += record.text;
|
|
1015
|
-
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
1016
|
-
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
updateRecentOutputLines();
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
1109
|
const resetRecentOutput = () => {
|
|
1023
1110
|
recentOutputTail = "";
|
|
1024
1111
|
progress.recentOutput = [];
|
|
@@ -1038,20 +1125,32 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1038
1125
|
});
|
|
1039
1126
|
};
|
|
1040
1127
|
|
|
1041
|
-
const processEvent = (event: AgentEvent) => {
|
|
1128
|
+
const processEvent = (event: AgentEvent, eventOrdinal: number) => {
|
|
1042
1129
|
if (resolved) return;
|
|
1043
1130
|
|
|
1044
1131
|
forwardSubagentEvent(event);
|
|
1045
1132
|
|
|
1046
1133
|
const now = Date.now();
|
|
1047
1134
|
let flushProgress = false;
|
|
1135
|
+
// A retry is recovered at the first assistant provider event, before the
|
|
1136
|
+
// session emits auto_retry_end after message completion.
|
|
1048
1137
|
|
|
1049
1138
|
switch (event.type) {
|
|
1050
|
-
case "message_start":
|
|
1051
|
-
if (event.message
|
|
1139
|
+
case "message_start": {
|
|
1140
|
+
if (event.message.role !== "assistant") break;
|
|
1141
|
+
const retryWasActive = Boolean(progress.retryState);
|
|
1142
|
+
const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
|
|
1143
|
+
rememberAssistantMessage(event.message);
|
|
1144
|
+
if (recoversRetry || !retryWasActive) {
|
|
1145
|
+
lastProviderProgressAtMs = now;
|
|
1052
1146
|
resetRecentOutput();
|
|
1053
1147
|
}
|
|
1148
|
+
if (recoversRetry) {
|
|
1149
|
+
progress.retryState = undefined;
|
|
1150
|
+
flushProgress = true;
|
|
1151
|
+
}
|
|
1054
1152
|
break;
|
|
1153
|
+
}
|
|
1055
1154
|
|
|
1056
1155
|
case "tool_execution_start": {
|
|
1057
1156
|
progress.toolCount++;
|
|
@@ -1163,24 +1262,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1163
1262
|
}
|
|
1164
1263
|
|
|
1165
1264
|
case "message_update": {
|
|
1166
|
-
if (event.message
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
break;
|
|
1175
|
-
}
|
|
1176
|
-
if (assistantEvent && assistantEvent.type !== "text_delta") {
|
|
1177
|
-
break;
|
|
1178
|
-
}
|
|
1179
|
-
const updateContent =
|
|
1180
|
-
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
1181
|
-
if (updateContent && Array.isArray(updateContent)) {
|
|
1182
|
-
replaceRecentOutputFromContent(updateContent);
|
|
1265
|
+
if (event.message.role !== "assistant") break;
|
|
1266
|
+
const retryWasActive = Boolean(progress.retryState);
|
|
1267
|
+
const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
|
|
1268
|
+
if (!retryWasActive) rememberAssistantMessage(event.message);
|
|
1269
|
+
if (recoversRetry || !retryWasActive) lastProviderProgressAtMs = now;
|
|
1270
|
+
if (recoversRetry) {
|
|
1271
|
+
progress.retryState = undefined;
|
|
1272
|
+
flushProgress = true;
|
|
1183
1273
|
}
|
|
1274
|
+
const textDelta = getProviderTextDelta(event);
|
|
1275
|
+
if (textDelta !== undefined) appendRecentOutputTail(textDelta);
|
|
1184
1276
|
break;
|
|
1185
1277
|
}
|
|
1186
1278
|
|
|
@@ -1188,6 +1280,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1188
1280
|
// Extract text from assistant and toolResult messages (not user prompts)
|
|
1189
1281
|
const role = event.message?.role;
|
|
1190
1282
|
if (role === "assistant") {
|
|
1283
|
+
if (isSuccessfulProviderAssistantMessage(event.message)) lastProviderProgressAtMs = now;
|
|
1191
1284
|
const messageContent =
|
|
1192
1285
|
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
1193
1286
|
if (messageContent && Array.isArray(messageContent)) {
|
|
@@ -1200,6 +1293,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1200
1293
|
const assistantModel = getMessageModelString(event.message);
|
|
1201
1294
|
if (assistantModel) {
|
|
1202
1295
|
lastAssistantModelString = assistantModel;
|
|
1296
|
+
activeProviderModelString = assistantModel;
|
|
1203
1297
|
if (resolvedModelString && assistantModel !== resolvedModelString && !modelSubstitutionWarning) {
|
|
1204
1298
|
modelSubstitutionWarning = {
|
|
1205
1299
|
requested: resolvedModelString,
|
|
@@ -1367,6 +1461,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1367
1461
|
);
|
|
1368
1462
|
if (model) {
|
|
1369
1463
|
resolvedModelString = formatModelString(model);
|
|
1464
|
+
activeProviderModelString = resolvedModelString;
|
|
1370
1465
|
}
|
|
1371
1466
|
if (authFallbackUsed && model && requestedModel) {
|
|
1372
1467
|
modelSubstitutionWarning = {
|
|
@@ -1504,6 +1599,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1504
1599
|
? { requestedModel, reason: modelSubstitutionWarning.reason }
|
|
1505
1600
|
: undefined,
|
|
1506
1601
|
toolNames,
|
|
1602
|
+
alwaysActiveToolNames: ircEnabled ? ["irc"] : undefined,
|
|
1507
1603
|
outputSchema,
|
|
1508
1604
|
requireYieldTool: true,
|
|
1509
1605
|
contextFiles: options.contextFiles,
|
|
@@ -1513,9 +1609,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1513
1609
|
systemPrompt: defaultPrompt => {
|
|
1514
1610
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1515
1611
|
agent: prompt.render(agent.systemPrompt, {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1612
|
+
// Typed executionMode wins; assignment text is compatibility-only (#2698 / #2456).
|
|
1613
|
+
ultragoalRedTeam: resolveUltragoalRedTeamActivation({
|
|
1614
|
+
executionMode: options.executionMode,
|
|
1615
|
+
assignment: options.assignment ?? task,
|
|
1616
|
+
}),
|
|
1519
1617
|
}),
|
|
1520
1618
|
context: options.context?.trim() ?? "",
|
|
1521
1619
|
worktree: worktree ?? "",
|
|
@@ -1737,11 +1835,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1737
1835
|
|
|
1738
1836
|
const MAX_YIELD_RETRIES = 3;
|
|
1739
1837
|
unsubscribe = session.subscribe(event => {
|
|
1838
|
+
sessionEventOrdinal += 1;
|
|
1839
|
+
const eventOrdinal = sessionEventOrdinal;
|
|
1740
1840
|
if (event.type === "auto_retry_start") {
|
|
1841
|
+
retryStartOrdinal = eventOrdinal;
|
|
1842
|
+
for (const message of session.messages) {
|
|
1843
|
+
if (message.role === "assistant") rememberAssistantMessage(message);
|
|
1844
|
+
}
|
|
1741
1845
|
progress.retryState = {
|
|
1742
1846
|
attempt: event.attempt,
|
|
1743
1847
|
maxAttempts: event.maxAttempts,
|
|
1744
1848
|
unbounded: event.unbounded,
|
|
1849
|
+
kind: classifyProviderRetry(event.errorMessage),
|
|
1850
|
+
provider: providerNameFromModel(
|
|
1851
|
+
activeProviderModelString ?? lastAssistantModelString ?? resolvedModelString,
|
|
1852
|
+
),
|
|
1853
|
+
lastProviderProgressAtMs,
|
|
1745
1854
|
delayMs: event.delayMs,
|
|
1746
1855
|
errorMessage: event.errorMessage,
|
|
1747
1856
|
startedAtMs: Date.now(),
|
|
@@ -1763,12 +1872,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1763
1872
|
return;
|
|
1764
1873
|
}
|
|
1765
1874
|
if (event.type === "model_fallback_switched") {
|
|
1875
|
+
activeProviderModelString = event.to;
|
|
1766
1876
|
forwardSubagentEvent(event);
|
|
1767
1877
|
return;
|
|
1768
1878
|
}
|
|
1769
1879
|
if (isAgentEvent(event)) {
|
|
1770
1880
|
try {
|
|
1771
|
-
processEvent(event);
|
|
1881
|
+
processEvent(event, eventOrdinal);
|
|
1772
1882
|
} catch (err) {
|
|
1773
1883
|
logger.error("Subagent event processing failed", {
|
|
1774
1884
|
error: err instanceof Error ? err.message : String(err),
|
package/src/task/index.ts
CHANGED
|
@@ -1446,6 +1446,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1446
1446
|
agent: effectiveAgent,
|
|
1447
1447
|
task: renderTaskAssignment(task.assignment, simpleMode),
|
|
1448
1448
|
assignment: task.assignment.trim(),
|
|
1449
|
+
executionMode: task.executionMode,
|
|
1449
1450
|
context: sharedContext,
|
|
1450
1451
|
description: task.description,
|
|
1451
1452
|
index,
|
|
@@ -1518,6 +1519,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1518
1519
|
agent: effectiveAgent,
|
|
1519
1520
|
task: renderTaskAssignment(task.assignment, simpleMode),
|
|
1520
1521
|
assignment: task.assignment.trim(),
|
|
1522
|
+
executionMode: task.executionMode,
|
|
1521
1523
|
context: sharedContext,
|
|
1522
1524
|
description: task.description,
|
|
1523
1525
|
index,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AgentProgress, TaskToolDetails } from "./types";
|
|
2
|
+
|
|
3
|
+
export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
|
|
4
|
+
|
|
5
|
+
const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
|
|
6
|
+
const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
|
|
7
|
+
|
|
8
|
+
export function classifyProviderRetry(errorMessage: string): ProviderRetryKind {
|
|
9
|
+
if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
|
|
10
|
+
if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
|
|
11
|
+
return "provider_error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function providerNameFromModel(model: string | undefined): string | undefined {
|
|
15
|
+
const provider = model?.split("/", 1)[0]?.trim();
|
|
16
|
+
return provider || undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function providerRetryPhaseLabel(kind: ProviderRetryKind): string {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "first_event_timeout":
|
|
22
|
+
return "first event timeout";
|
|
23
|
+
case "idle_stream_stall":
|
|
24
|
+
return "stream stalled";
|
|
25
|
+
case "provider_error":
|
|
26
|
+
return "provider error";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function providerProgressAgeLabel(retryState: NonNullable<AgentProgress["retryState"]>, nowMs: number): string {
|
|
31
|
+
if (retryState.lastProviderProgressAtMs === undefined) return "no provider events yet";
|
|
32
|
+
const ageSeconds = Math.max(0, Math.floor((nowMs - retryState.lastProviderProgressAtMs) / 1000));
|
|
33
|
+
return `last provider progress ${ageSeconds}s ago`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return Boolean(value) && typeof value === "object";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTaskToolDetails(value: unknown): value is TaskToolDetails {
|
|
41
|
+
return (
|
|
42
|
+
isRecord(value) && Array.isArray(value.results) && (value.progress === undefined || Array.isArray(value.progress))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isAgentProgress(value: unknown): value is AgentProgress {
|
|
47
|
+
return isRecord(value) && typeof value.status === "string";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hasActiveProviderRetryInTaskDetails(details: TaskToolDetails, seen = new WeakSet<object>()): boolean {
|
|
51
|
+
if (!isTaskToolDetails(details) || seen.has(details)) return false;
|
|
52
|
+
seen.add(details);
|
|
53
|
+
for (const progress of details.progress ?? []) {
|
|
54
|
+
if (isAgentProgress(progress) && hasActiveProviderRetryInProgress(progress, seen)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function hasActiveProviderRetryInProgress(progress: AgentProgress, seen = new WeakSet<object>()): boolean {
|
|
60
|
+
if (!isAgentProgress(progress) || seen.has(progress)) return false;
|
|
61
|
+
seen.add(progress);
|
|
62
|
+
if (progress.status === "running" && progress.retryState) return true;
|
|
63
|
+
|
|
64
|
+
const nestedTaskData = isRecord(progress.extractedToolData) ? progress.extractedToolData.task : undefined;
|
|
65
|
+
if (Array.isArray(nestedTaskData)) {
|
|
66
|
+
for (const nestedDetails of nestedTaskData) {
|
|
67
|
+
if (hasActiveProviderRetryInTaskDetails(nestedDetails as TaskToolDetails, seen)) return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return hasActiveProviderRetryInTaskDetails(progress.inflightTaskDetails as TaskToolDetails, seen);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ProviderDegradationGroup {
|
|
74
|
+
provider: string;
|
|
75
|
+
count: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function collectProviderDegradationGroups(progress: readonly AgentProgress[]): ProviderDegradationGroup[] {
|
|
79
|
+
const counts = new Map<string, number>();
|
|
80
|
+
for (const item of progress) {
|
|
81
|
+
if (!isAgentProgress(item) || item.status !== "running" || !item.retryState) continue;
|
|
82
|
+
const provider = item.retryState.provider ?? "provider";
|
|
83
|
+
counts.set(provider, (counts.get(provider) ?? 0) + 1);
|
|
84
|
+
}
|
|
85
|
+
return Array.from(counts, ([provider, count]) => ({ provider, count }))
|
|
86
|
+
.filter(group => group.count > 1)
|
|
87
|
+
.sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider));
|
|
88
|
+
}
|