@gajae-code/coding-agent 0.11.10 → 0.12.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/CHANGELOG.md +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- 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 +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- 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/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- 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/runtime-mcp/smithery-auth.d.ts +5 -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 +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- 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/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- 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/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- 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/lifecycle.ts +107 -7
- 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/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
package/src/task/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Usage } from "@gajae-code/ai";
|
|
3
|
-
import { $
|
|
3
|
+
import { $env } from "@gajae-code/utils";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
|
|
6
6
|
import type { TaskResultReceipt } from "./receipt";
|
|
@@ -14,23 +14,28 @@ export type AgentSource = "bundled" | "user" | "project";
|
|
|
14
14
|
export type ForkContextPolicy = "forbidden" | "allowed";
|
|
15
15
|
export type ForkContextMode = "none" | "receipt" | "last-turn" | "bounded" | "full";
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch {}
|
|
17
|
+
const parsePositiveIntegerEnvironment = (keys: string[], defaultValue: number): number => {
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const value = $env[key];
|
|
20
|
+
if (!value || value.trim().length === 0) continue;
|
|
21
|
+
if (!/^\d+$/.test(value)) return defaultValue;
|
|
22
|
+
const number = Number(value);
|
|
23
|
+
return Number.isSafeInteger(number) && number > 0 ? number : defaultValue;
|
|
25
24
|
}
|
|
26
25
|
return defaultValue;
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
/** Maximum output bytes per agent */
|
|
30
|
-
export const MAX_OUTPUT_BYTES =
|
|
29
|
+
export const MAX_OUTPUT_BYTES = parsePositiveIntegerEnvironment(
|
|
30
|
+
["GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"],
|
|
31
|
+
500_000,
|
|
32
|
+
);
|
|
31
33
|
|
|
32
34
|
/** Maximum output lines per agent */
|
|
33
|
-
export const MAX_OUTPUT_LINES =
|
|
35
|
+
export const MAX_OUTPUT_LINES = parsePositiveIntegerEnvironment(
|
|
36
|
+
["GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"],
|
|
37
|
+
5000,
|
|
38
|
+
);
|
|
34
39
|
|
|
35
40
|
/** EventBus channel for raw subagent events */
|
|
36
41
|
export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
|
|
@@ -92,6 +97,12 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
|
|
|
92
97
|
id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
|
|
93
98
|
description: z.string().describe("ui label, not seen by subagent"),
|
|
94
99
|
assignment: z.string().describe(assignmentDescription),
|
|
100
|
+
executionMode: z
|
|
101
|
+
.enum(["default", "ultragoal-red-team"])
|
|
102
|
+
.optional()
|
|
103
|
+
.describe(
|
|
104
|
+
"typed executor mode: default keeps ordinary executor behavior; ultragoal-red-team injects the Ultragoal QA/red-team prompt fragment. Prefer this over free-form assignment text (#2698).",
|
|
105
|
+
),
|
|
95
106
|
inheritContext: z
|
|
96
107
|
.enum(["none", "receipt", "last-turn", "bounded", "full"])
|
|
97
108
|
.optional()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether an executor assignment should inject the ultragoal red-team
|
|
3
|
+
* prompt fragment.
|
|
4
|
+
*
|
|
5
|
+
* Preference order (parse, don't re-validate later):
|
|
6
|
+
* 1. Explicit typed `executionMode` on the task / ExecutorOptions — authoritative.
|
|
7
|
+
* 2. Assignment text heuristics — only explicit Ultragoal QA/red-team phrasing.
|
|
8
|
+
*
|
|
9
|
+
* Bare mentions of `executorQa` (docs, quality-gate field names, negative
|
|
10
|
+
* instructions) must not flip the mode (#2698 / #2456).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `executor.md`:
|
|
13
|
+
* "activates only when the assignment explicitly labels Executor as Ultragoal
|
|
14
|
+
* completion QA/red-team or asks for `executorQa` red-team evidence."
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const EXECUTOR_EXECUTION_MODES = ["default", "ultragoal-red-team"] as const;
|
|
18
|
+
export type ExecutorExecutionMode = (typeof EXECUTOR_EXECUTION_MODES)[number];
|
|
19
|
+
|
|
20
|
+
const ULTRAGOAL_COMPLETION_QA = /\bultragoal\s+completion\s+(?:qa|red[-\s]?team)\b/i;
|
|
21
|
+
|
|
22
|
+
/** `executorQa` within a short window of red-team / matrix / evidence framing. */
|
|
23
|
+
const EXECUTOR_QA_EVIDENCE =
|
|
24
|
+
/\bexecutorQa\b[\s\S]{0,120}\b(?:red[-\s]?team|matrix|evidence)\b|\b(?:red[-\s]?team|matrix|evidence)\b[\s\S]{0,120}\bexecutorQa\b/i;
|
|
25
|
+
|
|
26
|
+
/** Common Ultragoal skill spawn phrasing: "executor QA/red-team lane". */
|
|
27
|
+
const EXECUTOR_QA_LANE = /\bexecutor\b[\s\S]{0,48}\b(?:qa|red[-\s]?team)\s+lane\b/i;
|
|
28
|
+
|
|
29
|
+
export function isExecutorExecutionMode(value: unknown): value is ExecutorExecutionMode {
|
|
30
|
+
return value === "default" || value === "ultragoal-red-team";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a caller-supplied mode. Unknown values fail closed to `undefined`
|
|
35
|
+
* so the assignment heuristic (or default off) still applies — never invent on.
|
|
36
|
+
*/
|
|
37
|
+
export function parseExecutorExecutionMode(value: unknown): ExecutorExecutionMode | undefined {
|
|
38
|
+
if (typeof value !== "string") return undefined;
|
|
39
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
40
|
+
if (normalized === "default" || normalized === "ordinary" || normalized === "implement") {
|
|
41
|
+
return "default";
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
normalized === "ultragoal-red-team" ||
|
|
45
|
+
normalized === "ultragoal-redteam" ||
|
|
46
|
+
normalized === "red-team" ||
|
|
47
|
+
normalized === "redteam" ||
|
|
48
|
+
normalized === "executor-qa"
|
|
49
|
+
) {
|
|
50
|
+
return "ultragoal-red-team";
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function assignmentRequestsUltragoalRedTeam(assignment: string | undefined): boolean {
|
|
56
|
+
const text = assignment?.trim() ?? "";
|
|
57
|
+
if (text.length === 0) return false;
|
|
58
|
+
return ULTRAGOAL_COMPLETION_QA.test(text) || EXECUTOR_QA_EVIDENCE.test(text) || EXECUTOR_QA_LANE.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ResolveUltragoalRedTeamArgs {
|
|
62
|
+
readonly executionMode?: unknown;
|
|
63
|
+
readonly assignment?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Final activation decision for the executor prompt fragment.
|
|
68
|
+
* Typed mode wins; assignment text is a compatibility fallback only.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveUltragoalRedTeamActivation(args: ResolveUltragoalRedTeamArgs): boolean {
|
|
71
|
+
const mode = parseExecutorExecutionMode(args.executionMode);
|
|
72
|
+
if (mode === "ultragoal-red-team") return true;
|
|
73
|
+
if (mode === "default") return false;
|
|
74
|
+
return assignmentRequestsUltragoalRedTeam(args.assignment);
|
|
75
|
+
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -351,15 +351,23 @@ function parseEncodedContainer(value: unknown): unknown {
|
|
|
351
351
|
function isRoundZeroRecoveryCandidate(value: unknown): boolean {
|
|
352
352
|
const root = parseEncodedContainer(value);
|
|
353
353
|
if (typeof root !== "object" || root === null || !Object.hasOwn(root, "questions")) return false;
|
|
354
|
-
const
|
|
354
|
+
const rawQuestions = (root as Record<string, unknown>).questions;
|
|
355
|
+
const questionsValue = parseEncodedContainer(rawQuestions);
|
|
355
356
|
if (!Array.isArray(questionsValue)) return questionsValue === null;
|
|
357
|
+
const rootEncoded = typeof value === "string" || typeof rawQuestions === "string";
|
|
356
358
|
return questionsValue.some(rawQuestion => {
|
|
357
359
|
const question = parseEncodedContainer(rawQuestion);
|
|
358
360
|
if (typeof question !== "object" || question === null || !Object.hasOwn(question, "deepInterview")) return false;
|
|
359
|
-
const
|
|
361
|
+
const rawDeepInterview = (question as Record<string, unknown>).deepInterview;
|
|
362
|
+
const deepInterview = parseEncodedContainer(rawDeepInterview);
|
|
360
363
|
if (typeof deepInterview !== "object" || deepInterview === null) return false;
|
|
361
364
|
const metadata = deepInterview as Record<string, unknown>;
|
|
362
|
-
|
|
365
|
+
const hasContract = Object.hasOwn(metadata, "intent_contract");
|
|
366
|
+
const hasReview = Object.hasOwn(metadata, "intent_review");
|
|
367
|
+
// A JSON-string container is terminal only for the retired contract+review
|
|
368
|
+
// pair; canonical single-sided asks stay eligible for generic JSON coercion.
|
|
369
|
+
const encoded = rootEncoded || typeof rawQuestion === "string" || typeof rawDeepInterview === "string";
|
|
370
|
+
return encoded ? hasContract && hasReview : hasContract || hasReview;
|
|
363
371
|
});
|
|
364
372
|
}
|
|
365
373
|
|
|
@@ -1252,6 +1260,14 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1252
1260
|
receipt: AskRemoteReceipt;
|
|
1253
1261
|
settlement?: AskSettlement;
|
|
1254
1262
|
};
|
|
1263
|
+
const ignoreRemoteAnswerFailure = (error: unknown): Promise<never> => {
|
|
1264
|
+
if (!(error instanceof Error && error.name === "AbortError")) {
|
|
1265
|
+
logger.warn("Ask remote answer source failed", {
|
|
1266
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
return new Promise<never>(() => {});
|
|
1270
|
+
};
|
|
1255
1271
|
const settleActiveRemote = async (settlement: AskSettlement): Promise<void> => {
|
|
1256
1272
|
const receipt = activeRemoteReceipt;
|
|
1257
1273
|
activeRemoteReceipt = undefined;
|
|
@@ -1304,60 +1320,62 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1304
1320
|
remoteController.signal,
|
|
1305
1321
|
)
|
|
1306
1322
|
: source.awaitAnswer(prompt, options, remoteController.signal)
|
|
1307
|
-
)
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
option
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1323
|
+
)
|
|
1324
|
+
.then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
|
|
1325
|
+
if (answer === undefined) return new Promise<never>(() => {});
|
|
1326
|
+
const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
|
|
1327
|
+
if (generation !== remoteGeneration) {
|
|
1328
|
+
return receipt
|
|
1329
|
+
.settle({ kind: "resolve_without_commit", reason: "aborted" })
|
|
1330
|
+
.then(() => new Promise<never>(() => {}));
|
|
1331
|
+
}
|
|
1332
|
+
const remoteValue = receipt.interaction.kind === "value" ? receipt.interaction.value : undefined;
|
|
1333
|
+
const value = remoteValue ?? REMOTE_NAVIGATION_FORWARD;
|
|
1334
|
+
const selectedValue =
|
|
1335
|
+
remoteValue === undefined
|
|
1336
|
+
? value
|
|
1337
|
+
: (options.find(
|
|
1338
|
+
option =>
|
|
1339
|
+
option === remoteValue ||
|
|
1340
|
+
option === `${theme.checkbox.checked} ${remoteValue}` ||
|
|
1341
|
+
option === `${theme.checkbox.unchecked} ${remoteValue}`,
|
|
1342
|
+
) ?? value);
|
|
1343
|
+
const normalizedRemoteValue = remoteValue?.startsWith(`${theme.checkbox.checked} `)
|
|
1344
|
+
? remoteValue.slice(`${theme.checkbox.checked} `.length)
|
|
1345
|
+
: remoteValue?.startsWith(`${theme.checkbox.unchecked} `)
|
|
1346
|
+
? remoteValue.slice(`${theme.checkbox.unchecked} `.length)
|
|
1347
|
+
: remoteValue;
|
|
1348
|
+
const semanticRemoteValue = normalizedRemoteValue?.replace(/^\s*\d+[.)]\s+/, "");
|
|
1349
|
+
const transitionReason =
|
|
1350
|
+
semanticRemoteValue === OTHER_OPTION
|
|
1351
|
+
? "other_transition"
|
|
1352
|
+
: semanticRemoteValue === ASK_CLARIFICATION_OPTION
|
|
1353
|
+
? "clarification_transition"
|
|
1354
|
+
: undefined;
|
|
1355
|
+
if (transitionReason) {
|
|
1356
|
+
return {
|
|
1357
|
+
winner: "remote" as const,
|
|
1358
|
+
value: selectedValue,
|
|
1359
|
+
receipt,
|
|
1360
|
+
settlement: { kind: "resolve_without_commit", reason: transitionReason },
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
if (
|
|
1364
|
+
remoteValue !== undefined &&
|
|
1365
|
+
activeRemoteRequest?.interaction === "selector" &&
|
|
1366
|
+
activeRemoteRequest.controls.length > 0 &&
|
|
1367
|
+
activeRemoteRequest.options.includes(remoteValue)
|
|
1368
|
+
) {
|
|
1369
|
+
return {
|
|
1370
|
+
winner: "remote" as const,
|
|
1371
|
+
value: selectedValue,
|
|
1372
|
+
receipt,
|
|
1373
|
+
settlement: { kind: "resolve_without_commit", reason: "toggle" },
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
return { winner: "remote" as const, value: selectedValue, receipt };
|
|
1377
|
+
})
|
|
1378
|
+
.catch(ignoreRemoteAnswerFailure);
|
|
1361
1379
|
|
|
1362
1380
|
const local = extensionUi
|
|
1363
1381
|
.select(prompt, options, { ...dialogOptions, signal: localController.signal })
|
|
@@ -1419,18 +1437,20 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1419
1437
|
remoteController.signal,
|
|
1420
1438
|
)
|
|
1421
1439
|
: source.awaitAnswer(title, [], remoteController.signal)
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1440
|
+
)
|
|
1441
|
+
.then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
|
|
1442
|
+
if (answer === undefined) return new Promise<never>(() => {});
|
|
1443
|
+
const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
|
|
1444
|
+
if (generation !== remoteGeneration) {
|
|
1445
|
+
return receipt
|
|
1446
|
+
.settle({ kind: "resolve_without_commit", reason: "aborted" })
|
|
1447
|
+
.then(() => new Promise<never>(() => {}));
|
|
1448
|
+
}
|
|
1449
|
+
const value =
|
|
1450
|
+
receipt.interaction.kind === "control" ? REMOTE_NAVIGATION_FORWARD : receipt.interaction.value;
|
|
1451
|
+
return { winner: "remote" as const, value, receipt };
|
|
1452
|
+
})
|
|
1453
|
+
.catch(ignoreRemoteAnswerFailure);
|
|
1434
1454
|
const local = extensionUi
|
|
1435
1455
|
.editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
|
|
1436
1456
|
.then(answer => {
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getPuppeteerDir } from "@gajae-code/utils/dirs";
|
|
5
|
+
import { $credentialEnv } from "@gajae-code/utils/env";
|
|
5
6
|
import * as logger from "@gajae-code/utils/logger";
|
|
6
7
|
import { $which } from "@gajae-code/utils/which";
|
|
7
8
|
import * as browsers from "@puppeteer/browsers";
|
|
@@ -80,6 +81,42 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Browser launch overrides resolved from trusted environment sources only.
|
|
86
|
+
*
|
|
87
|
+
* These choose the browser binary and route/verify its traffic, so whatever can
|
|
88
|
+
* set them controls what runs and who sees the session. `$env` merges the
|
|
89
|
+
* caller's `cwd/.env` into `process.env`, so reading them directly would let
|
|
90
|
+
* repository content pick the executable, point every request at an attacker's
|
|
91
|
+
* proxy, or disable certificate validation. Resolve them the same way provider
|
|
92
|
+
* credentials are: launching shell plus GJC/user-owned `.env` files, never the
|
|
93
|
+
* project `.env`.
|
|
94
|
+
*/
|
|
95
|
+
function trustedBrowserEnv(name: string): string | undefined {
|
|
96
|
+
return $credentialEnv(name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Boolean-like trusted browser env knob (`true`/`1`/`yes`/`on`). */
|
|
100
|
+
function browserLaunchFlagEnabled(name: string): boolean {
|
|
101
|
+
const value = trustedBrowserEnv(name)?.toLowerCase();
|
|
102
|
+
return value === "true" || value === "1" || value === "yes" || value === "on";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Test seam: the browser launch overrides as resolved from trusted env. */
|
|
106
|
+
export function resolveBrowserEnvOverridesForTest(): {
|
|
107
|
+
executablePath: string | undefined;
|
|
108
|
+
proxy: string | undefined;
|
|
109
|
+
proxyBypassLoopback: boolean;
|
|
110
|
+
ignoreCertErrors: boolean;
|
|
111
|
+
} {
|
|
112
|
+
return {
|
|
113
|
+
executablePath: trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH"),
|
|
114
|
+
proxy: trustedBrowserEnv("PUPPETEER_PROXY"),
|
|
115
|
+
proxyBypassLoopback: browserLaunchFlagEnabled("PUPPETEER_PROXY_BYPASS_LOOPBACK"),
|
|
116
|
+
ignoreCertErrors: browserLaunchFlagEnabled("PUPPETEER_PROXY_IGNORE_CERT_ERRORS"),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
/**
|
|
84
121
|
* Lazily download Chromium on first browser launch via @puppeteer/browsers.
|
|
85
122
|
* Skipped when a system Chromium (NixOS) or PUPPETEER_EXECUTABLE_PATH is set.
|
|
@@ -89,7 +126,7 @@ let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
|
89
126
|
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
90
127
|
const sysChrome = resolveSystemChromium();
|
|
91
128
|
if (sysChrome) return sysChrome;
|
|
92
|
-
const envPath =
|
|
129
|
+
const envPath = trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH");
|
|
93
130
|
if (envPath) return envPath;
|
|
94
131
|
if (chromiumExecutablePromise) return chromiumExecutablePromise;
|
|
95
132
|
|
|
@@ -272,13 +309,12 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
272
309
|
// Isolated copy only (caller guarantees). Never point this at a live profile.
|
|
273
310
|
launchArgs.push(`--user-data-dir=${opts.profileWarmupDir}`);
|
|
274
311
|
}
|
|
275
|
-
const proxy =
|
|
312
|
+
const proxy = trustedBrowserEnv("PUPPETEER_PROXY");
|
|
276
313
|
if (proxy) {
|
|
277
314
|
launchArgs.push(`--proxy-server=${proxy}`);
|
|
278
315
|
// Chrome (since v72) bypasses proxies for localhost by default. When PUPPETEER_PROXY_BYPASS_LOOPBACK
|
|
279
316
|
// is true, add <-loopback> so traffic to localhost reaches the proxy (e.g. for mitmdump/auth capture).
|
|
280
|
-
|
|
281
|
-
if (bypassLoopback === "true" || bypassLoopback === "1" || bypassLoopback === "yes" || bypassLoopback === "on") {
|
|
317
|
+
if (browserLaunchFlagEnabled("PUPPETEER_PROXY_BYPASS_LOOPBACK")) {
|
|
282
318
|
launchArgs.push("--proxy-bypass-list=<-loopback>");
|
|
283
319
|
}
|
|
284
320
|
}
|
|
@@ -287,8 +323,7 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
287
323
|
if (opts.geo?.locale) {
|
|
288
324
|
launchArgs.push(`--lang=${opts.geo.locale}`);
|
|
289
325
|
}
|
|
290
|
-
|
|
291
|
-
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
326
|
+
if (browserLaunchFlagEnabled("PUPPETEER_PROXY_IGNORE_CERT_ERRORS")) {
|
|
292
327
|
launchArgs.push("--ignore-certificate-errors");
|
|
293
328
|
}
|
|
294
329
|
const launchEnv = opts.geo?.timezone ? { ...process.env, TZ: opts.geo.timezone } : undefined;
|
package/src/tools/fetch.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
10
10
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
11
11
|
import type { ToolSession } from "../sdk";
|
|
12
12
|
import type { AgentStorage } from "../session/agent-storage";
|
|
13
|
-
import { DEFAULT_MAX_BYTES,
|
|
13
|
+
import { DEFAULT_MAX_BYTES, type TruncationDirection, truncateContent } from "../session/streaming-output";
|
|
14
14
|
import { renderStatusLine } from "../tui";
|
|
15
15
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
16
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
@@ -1146,7 +1146,12 @@ interface ReadUrlCacheEntry {
|
|
|
1146
1146
|
artifactId?: string;
|
|
1147
1147
|
details: ReadUrlToolDetails;
|
|
1148
1148
|
image?: FetchImagePayload;
|
|
1149
|
+
/** Complete URL output, including its preamble and body. */
|
|
1149
1150
|
output: string;
|
|
1151
|
+
/** UTF-16 code-unit offset of the preamble in `output`. */
|
|
1152
|
+
preambleChars: number;
|
|
1153
|
+
/** Offset of the preamble after `wrapUntrustedContent(output)` is persisted. */
|
|
1154
|
+
wrappedPreambleChars?: number;
|
|
1150
1155
|
}
|
|
1151
1156
|
|
|
1152
1157
|
const readUrlCache = new Map<string, ReadUrlCacheEntry>();
|
|
@@ -1178,7 +1183,13 @@ async function materializeReadUrlCacheEntry(
|
|
|
1178
1183
|
if (entry.artifactId) {
|
|
1179
1184
|
const artifactOutput = await readArtifactOutput(session, entry.artifactId);
|
|
1180
1185
|
if (artifactOutput !== null) {
|
|
1181
|
-
|
|
1186
|
+
// Artifacts retain their trust-boundary wrapper. Keep the raw preamble
|
|
1187
|
+
// coordinate alongside the escaped coordinate for live/rehydrated parity.
|
|
1188
|
+
return {
|
|
1189
|
+
...entry,
|
|
1190
|
+
output: artifactOutput,
|
|
1191
|
+
...(entry.wrappedPreambleChars !== undefined ? { wrappedPreambleChars: entry.wrappedPreambleChars } : {}),
|
|
1192
|
+
};
|
|
1182
1193
|
}
|
|
1183
1194
|
}
|
|
1184
1195
|
return null;
|
|
@@ -1218,8 +1229,8 @@ async function buildReadUrlCacheEntry(
|
|
|
1218
1229
|
|
|
1219
1230
|
const storage = session.settings.getStorage();
|
|
1220
1231
|
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
|
|
1221
|
-
const
|
|
1222
|
-
const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
|
|
1232
|
+
const built = buildUrlReadOutput(result, result.content);
|
|
1233
|
+
const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, built.output) : undefined;
|
|
1223
1234
|
|
|
1224
1235
|
return {
|
|
1225
1236
|
artifactId,
|
|
@@ -1233,7 +1244,9 @@ async function buildReadUrlCacheEntry(
|
|
|
1233
1244
|
notes: result.notes,
|
|
1234
1245
|
},
|
|
1235
1246
|
image: result.image,
|
|
1236
|
-
output,
|
|
1247
|
+
output: built.output,
|
|
1248
|
+
preambleChars: built.preambleChars,
|
|
1249
|
+
wrappedPreambleChars: built.wrappedPreambleChars,
|
|
1237
1250
|
};
|
|
1238
1251
|
}
|
|
1239
1252
|
|
|
@@ -1263,60 +1276,138 @@ export async function loadReadUrlCacheEntry(
|
|
|
1263
1276
|
|
|
1264
1277
|
const UNTRUSTED_CONTENT_OPEN = "<untrusted-content>";
|
|
1265
1278
|
const UNTRUSTED_CONTENT_CLOSE = "</untrusted-content>";
|
|
1279
|
+
export const WRAP_PREFIX_CHARS = `${UNTRUSTED_CONTENT_OPEN}\n`.length;
|
|
1280
|
+
const WRAP_SUFFIX = `\n${UNTRUSTED_CONTENT_CLOSE}`;
|
|
1281
|
+
|
|
1282
|
+
export function escapeUntrustedContent(content: string): string {
|
|
1283
|
+
return content.replace(/<\/untrusted-content>/gi, "</untrusted-content>");
|
|
1284
|
+
}
|
|
1266
1285
|
|
|
1267
1286
|
export function wrapUntrustedContent(content: string): string {
|
|
1268
|
-
return `${UNTRUSTED_CONTENT_OPEN}\n${
|
|
1287
|
+
return `${UNTRUSTED_CONTENT_OPEN}\n${escapeUntrustedContent(content)}${WRAP_SUFFIX}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function isWrappedReadUrlOutput(output: string): boolean {
|
|
1291
|
+
return output.startsWith(`${UNTRUSTED_CONTENT_OPEN}\n`) && output.endsWith(WRAP_SUFFIX);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Remove only the outer URL-output wrapper before line pagination. The content
|
|
1296
|
+
* remains escaped exactly as persisted; it is deliberately never unescaped.
|
|
1297
|
+
*/
|
|
1298
|
+
export function prepareReadUrlSelectorInput(
|
|
1299
|
+
output: string,
|
|
1300
|
+
preambleChars: number,
|
|
1301
|
+
wrappedPreambleChars?: number,
|
|
1302
|
+
): { text: string; preambleChars: number } {
|
|
1303
|
+
if (!isWrappedReadUrlOutput(output)) return { text: output, preambleChars };
|
|
1304
|
+
const bodyEnd = output.length - WRAP_SUFFIX.length;
|
|
1305
|
+
return {
|
|
1306
|
+
text: output.slice(WRAP_PREFIX_CHARS, Math.max(WRAP_PREFIX_CHARS, bodyEnd)),
|
|
1307
|
+
preambleChars: Math.max(0, (wrappedPreambleChars ?? preambleChars) - WRAP_PREFIX_CHARS),
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
interface BuiltUrlReadOutput {
|
|
1311
|
+
output: string;
|
|
1312
|
+
preambleChars: number;
|
|
1313
|
+
wrappedPreambleChars: number;
|
|
1269
1314
|
}
|
|
1270
1315
|
|
|
1271
|
-
function
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1316
|
+
function buildReadUrlPreamble(
|
|
1317
|
+
result: Pick<FetchRenderResult, "finalUrl" | "contentType" | "method" | "notes">,
|
|
1318
|
+
): string {
|
|
1319
|
+
let preamble = "";
|
|
1320
|
+
preamble += `URL: ${result.finalUrl}\n`;
|
|
1321
|
+
preamble += `Content-Type: ${result.contentType}\n`;
|
|
1322
|
+
preamble += `Method: ${result.method}\n`;
|
|
1276
1323
|
if (result.notes.length > 0) {
|
|
1277
|
-
|
|
1324
|
+
preamble += `Notes: ${result.notes.join("; ")}\n`;
|
|
1278
1325
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1326
|
+
preamble += "\n---\n\n";
|
|
1327
|
+
return preamble;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function buildUrlReadOutput(result: FetchRenderResult, content: string): BuiltUrlReadOutput {
|
|
1331
|
+
const preamble = buildReadUrlPreamble(result);
|
|
1332
|
+
const preambleChars = preamble.length;
|
|
1333
|
+
const output = `${preamble}${content}`;
|
|
1334
|
+
return {
|
|
1335
|
+
output,
|
|
1336
|
+
preambleChars,
|
|
1337
|
+
wrappedPreambleChars: WRAP_PREFIX_CHARS + escapeUntrustedContent(preamble).length,
|
|
1338
|
+
};
|
|
1282
1339
|
}
|
|
1283
1340
|
|
|
1284
1341
|
export async function executeReadUrl(
|
|
1285
1342
|
session: ToolSession,
|
|
1286
1343
|
params: { path: string; raw?: boolean },
|
|
1287
1344
|
signal?: AbortSignal,
|
|
1345
|
+
direction: TruncationDirection = "head",
|
|
1288
1346
|
): Promise<AgentToolResult<ReadUrlToolDetails>> {
|
|
1289
1347
|
let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
|
|
1290
|
-
|
|
1348
|
+
// `head` intentionally retains the historical whole-output accounting so the
|
|
1349
|
+
// existing URL golden remains byte-identical. Non-head directions account for
|
|
1350
|
+
// and truncate only the body; preamble bytes/chars are never part of that cap.
|
|
1351
|
+
const effectiveDirection =
|
|
1352
|
+
direction !== "head" && cacheEntry.wrappedPreambleChars === undefined ? "head" : direction;
|
|
1353
|
+
const isWrappedOutput = isWrappedReadUrlOutput(cacheEntry.output);
|
|
1354
|
+
const preambleChars = isWrappedOutput
|
|
1355
|
+
? (cacheEntry.wrappedPreambleChars ??
|
|
1356
|
+
WRAP_PREFIX_CHARS + escapeUntrustedContent(buildReadUrlPreamble(cacheEntry.details)).length)
|
|
1357
|
+
: cacheEntry.preambleChars;
|
|
1358
|
+
const wrappedPrefix = isWrappedOutput ? WRAP_PREFIX_CHARS : 0;
|
|
1359
|
+
const wrappedSuffix = isWrappedOutput ? `\n${UNTRUSTED_CONTENT_CLOSE}` : "";
|
|
1360
|
+
const bodyEnd = cacheEntry.output.length - wrappedSuffix.length;
|
|
1361
|
+
const unwrappedOutput = isWrappedOutput
|
|
1362
|
+
? cacheEntry.output.slice(wrappedPrefix, Math.max(wrappedPrefix, bodyEnd))
|
|
1363
|
+
: cacheEntry.output;
|
|
1364
|
+
const truncationInput =
|
|
1365
|
+
effectiveDirection === "head"
|
|
1366
|
+
? unwrappedOutput
|
|
1367
|
+
: cacheEntry.output.slice(preambleChars, Math.max(preambleChars, bodyEnd));
|
|
1368
|
+
const truncation = truncateContent(truncationInput, {
|
|
1291
1369
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
1292
1370
|
maxLines: FETCH_DEFAULT_MAX_LINES,
|
|
1371
|
+
direction: effectiveDirection,
|
|
1293
1372
|
});
|
|
1294
1373
|
const needsArtifact = truncation.truncated;
|
|
1295
1374
|
if (needsArtifact && !cacheEntry.artifactId) {
|
|
1296
1375
|
cacheEntry = await ensureReadUrlCacheArtifact(session, cacheEntry);
|
|
1297
1376
|
cacheReadUrlEntry(session, params.path, params.raw ?? false, cacheEntry);
|
|
1298
1377
|
}
|
|
1299
|
-
const output =
|
|
1378
|
+
const output =
|
|
1379
|
+
effectiveDirection !== "head"
|
|
1380
|
+
? needsArtifact
|
|
1381
|
+
? `${cacheEntry.output.slice(0, preambleChars)}${truncation.content}${wrappedSuffix}`
|
|
1382
|
+
: cacheEntry.output
|
|
1383
|
+
: truncation.content;
|
|
1300
1384
|
const details: ReadUrlToolDetails = {
|
|
1301
1385
|
...cacheEntry.details,
|
|
1302
1386
|
truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
|
|
1303
1387
|
};
|
|
1304
1388
|
|
|
1305
|
-
const contentBlocks: Array<TextContent | ImageContent> = [
|
|
1389
|
+
const contentBlocks: Array<TextContent | ImageContent> = [
|
|
1390
|
+
{ type: "text", text: isWrappedOutput && effectiveDirection !== "head" ? output : wrapUntrustedContent(output) },
|
|
1391
|
+
];
|
|
1306
1392
|
if (cacheEntry.image) {
|
|
1307
1393
|
contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
|
|
1308
1394
|
}
|
|
1309
1395
|
|
|
1310
1396
|
const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(details.finalUrl);
|
|
1311
1397
|
if (needsArtifact) {
|
|
1312
|
-
resultBuilder.truncation(truncation, {
|
|
1398
|
+
resultBuilder.truncation(truncation, {
|
|
1399
|
+
direction: effectiveDirection === "both" ? "middle" : effectiveDirection === "last" ? "tail" : "head",
|
|
1400
|
+
artifactId: cacheEntry.artifactId,
|
|
1401
|
+
...(effectiveDirection !== "head" ? { maxBytes: DEFAULT_MAX_BYTES } : {}),
|
|
1402
|
+
});
|
|
1313
1403
|
} else if (cacheEntry.details.truncated) {
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1404
|
+
const reportedText = effectiveDirection === "head" ? cacheEntry.output : truncationInput;
|
|
1405
|
+
const outputLines = reportedText.split("\n").length;
|
|
1406
|
+
const outputBytes = Buffer.byteLength(reportedText, "utf-8");
|
|
1316
1407
|
const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
|
|
1317
1408
|
const totalLines = outputLines + 1;
|
|
1318
|
-
resultBuilder.truncationFromText(
|
|
1319
|
-
direction: "tail",
|
|
1409
|
+
resultBuilder.truncationFromText(reportedText, {
|
|
1410
|
+
direction: effectiveDirection === "both" ? "middle" : effectiveDirection === "last" ? "tail" : "head",
|
|
1320
1411
|
totalLines,
|
|
1321
1412
|
totalBytes,
|
|
1322
1413
|
maxBytes: MAX_OUTPUT_CHARS,
|