@stigmer/runner 3.11.0 → 3.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/README.md +2 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +6 -4
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
- package/dist/activities/execute-cursor/error-classifier.js +34 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +52 -12
- package/dist/activities/execute-cursor/index.js +141 -67
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -11
- package/dist/activities/execute-cursor/prompt-builder.js +57 -10
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +11 -0
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +0 -15
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
- package/dist/activities/execute-deep-agent/setup.js +22 -49
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +7 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -5
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -26
- package/dist/client/stigmer-client.js +88 -24
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.js.map +1 -1
- package/dist/encryption/config.d.ts +41 -9
- package/dist/encryption/config.js +63 -23
- package/dist/encryption/config.js.map +1 -1
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +2 -1
- package/dist/payload-codecs.js +7 -3
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +17 -3
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +7 -3
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +34 -1
- package/dist/shared/artifact-storage.js +53 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-download-urls.d.ts +62 -0
- package/dist/shared/attachment-download-urls.js +79 -0
- package/dist/shared/attachment-download-urls.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +4 -20
- package/dist/shared/checkpointer/http-saver.js +15 -57
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.d.ts +2 -3
- package/dist/shared/conversation-attachment.js +2 -3
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +51 -9
- package/dist/shared/http-retry.js +60 -9
- package/dist/shared/http-retry.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +8 -4
- package/dist/shared/mcp-manager.js +30 -4
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/mcp-transport-guard.d.ts +2 -2
- package/dist/shared/mcp-transport-guard.js +2 -2
- package/dist/shared/model-client.d.ts +6 -0
- package/dist/shared/model-client.js +14 -3
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +7 -0
- package/dist/shared/plan-mode-permissions.js +7 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/shared/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +2 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +3 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/fake-artifact-storage.ts +10 -1
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/encryption-codec.test.ts +53 -0
- package/src/__tests__/preflight.test.ts +15 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/discover-mcp-server.ts +16 -2
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +173 -31
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
- package/src/activities/execute-cursor/error-classifier.ts +40 -3
- package/src/activities/execute-cursor/index.ts +175 -74
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +89 -22
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +71 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +22 -17
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
- package/src/activities/execute-deep-agent/index.ts +0 -19
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
- package/src/activities/execute-deep-agent/setup.ts +22 -73
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
- package/src/activities/hydrate-workflow-execution.ts +8 -5
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/bootstrap.ts +5 -0
- package/src/client/__tests__/stigmer-client.test.ts +104 -5
- package/src/client/stigmer-client.ts +128 -29
- package/src/config.ts +6 -5
- package/src/encryption/config.ts +87 -23
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +8 -2
- package/src/runner-manager.ts +21 -3
- package/src/runner.ts +7 -3
- package/src/shared/__tests__/artifact-storage.test.ts +154 -2
- package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
- package/src/shared/__tests__/channel-attachment.test.ts +2 -2
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/http-retry.test.ts +5 -4
- package/src/shared/__tests__/mcp-manager.test.ts +53 -13
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +46 -0
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
- package/src/shared/artifact-storage.ts +86 -10
- package/src/shared/attachment-download-urls.ts +98 -0
- package/src/shared/channel-attachment.ts +1 -1
- package/src/shared/checkpointer/http-saver.ts +15 -60
- package/src/shared/conversation-attachment.ts +2 -3
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +98 -9
- package/src/shared/mcp-manager.ts +35 -4
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/model-client.ts +21 -3
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +7 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/shared/synthesized-attachment.ts +4 -4
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/connect-mcp-server.ts +2 -0
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/dist/shared/datastore-attachment.d.ts +0 -99
- package/dist/shared/datastore-attachment.js +0 -184
- package/dist/shared/datastore-attachment.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
- package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
- package/src/shared/datastore-attachment.ts +0 -209
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-recovery digest: the platform's recorded transcript of the current
|
|
3
|
+
* turn, rendered as prompt context for a fresh agent that replaced one whose
|
|
4
|
+
* conversation state was lost mid-turn (issue #366).
|
|
5
|
+
*
|
|
6
|
+
* A HITL-paused turn can lose its agent two ways — the stored handle fails
|
|
7
|
+
* to resume when the approval lands, or the resumed handle turns out to be
|
|
8
|
+
* poisoned mid-send. Either way the replacement agent starts with an EMPTY
|
|
9
|
+
* conversation, so the approval decisions alone read as instructions with no
|
|
10
|
+
* story: the agent no longer knows what it was doing. This module rebuilds
|
|
11
|
+
* that story from `status.messages` — the persisted execution transcript
|
|
12
|
+
* seeded on every re-invocation — which is the platform's durable record of
|
|
13
|
+
* the turn AND exactly what the user watched happen (introspecting the dead
|
|
14
|
+
* agent's own conversation is best-effort at most; the handle is the thing
|
|
15
|
+
* that failed).
|
|
16
|
+
*
|
|
17
|
+
* Doctrine (mirrors `ChannelRolloverBridgeComposer`, cloud DD-013 — the
|
|
18
|
+
* platform's reference for conversation digests):
|
|
19
|
+
* - Content here, presentation framing separate: {@link composeTurnRecoveryDigest}
|
|
20
|
+
* emits bare `Assistant:` / `Tool:` / `System:` lines;
|
|
21
|
+
* {@link formatTurnRecoveryText} adds the behavioral preamble; the prompt
|
|
22
|
+
* builder wraps the section tag (the context-bridge / conversation-catchup
|
|
23
|
+
* split).
|
|
24
|
+
* - Oldest-first reading order; over budget, the OLDEST lines drop first
|
|
25
|
+
* (recency wins) and the drop is disclosed.
|
|
26
|
+
* - Best-effort by contract: composition never throws — a failed digest
|
|
27
|
+
* degrades to recovery-without-transcript, never a failed recovery.
|
|
28
|
+
*
|
|
29
|
+
* Scope: the CURRENT turn only. Prior turns live in other execution records
|
|
30
|
+
* and are not recoverable runner-side (that is the rollover bridge's domain,
|
|
31
|
+
* composed cloud-side); the enhanced prompt's standing-context sections carry
|
|
32
|
+
* whatever the session already had.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
36
|
+
import { MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
37
|
+
|
|
38
|
+
/** Per-line text budget; longer texts are cut with an ellipsis (DD-013 twin). */
|
|
39
|
+
const MAX_LINE_CHARS = 400;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Args-preview budget inside a tool line — the `describeApproval` precedent
|
|
43
|
+
* (prompt-builder.ts), so tool renderings are bounded the same way on both
|
|
44
|
+
* surfaces that describe them.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_ARGS_CHARS = 200;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whole-digest budget (DD-013 twin). Enforced by dropping the OLDEST lines
|
|
50
|
+
* first — recency wins, matching how a human skims their own recent work
|
|
51
|
+
* before continuing it.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_DIGEST_CHARS = 4000;
|
|
54
|
+
|
|
55
|
+
const ELLIPSIS = "\u2026";
|
|
56
|
+
|
|
57
|
+
/** Disclosed in place of the dropped prefix so the record never lies by omission. */
|
|
58
|
+
const OMISSION_NOTICE = `[${ELLIPSIS} earlier activity in this turn omitted for length]`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How the transcript is introduced to the model. Establishes the three facts
|
|
62
|
+
* the replacement agent must hold: the work below is its OWN (continue, don't
|
|
63
|
+
* restart), completed actions are DONE (don't redo them), and the workspace —
|
|
64
|
+
* not this summary — is the source of truth for exact current state.
|
|
65
|
+
*/
|
|
66
|
+
const TURN_RECOVERY_PREAMBLE =
|
|
67
|
+
"You had already started working on the user's request above, but the " +
|
|
68
|
+
"session holding that conversation was lost, so you do not remember it. " +
|
|
69
|
+
"Below is the platform's recorded transcript of your progress in this " +
|
|
70
|
+
"turn, oldest first. Treat it as work YOU already did: do not start the " +
|
|
71
|
+
"task over, do not redo actions shown as completed, and check the " +
|
|
72
|
+
"workspace's current state where exact details matter.";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fallback framing when the turn produced no renderable transcript (or
|
|
76
|
+
* composition failed): the state-loss disclosure still matters — without it,
|
|
77
|
+
* the appended approval decisions read as reactions to proposals this agent
|
|
78
|
+
* never made.
|
|
79
|
+
*/
|
|
80
|
+
const TURN_RECOVERY_NO_TRANSCRIPT =
|
|
81
|
+
"You had already started working on the user's request above, but the " +
|
|
82
|
+
"session holding that conversation was lost, so you do not remember it " +
|
|
83
|
+
"and no transcript of your progress is available. Check the workspace's " +
|
|
84
|
+
"current state to see what you already did before continuing.";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render the turn's persisted transcript as bare digest lines, or undefined
|
|
88
|
+
* when there is nothing worth carrying. Never throws.
|
|
89
|
+
*
|
|
90
|
+
* Human messages are skipped — the turn's user message is already rendered
|
|
91
|
+
* in the enhanced prompt's `<user_request>`, and duplicating it here would
|
|
92
|
+
* dilute the budget. Thinking content is skipped as model-internal (the
|
|
93
|
+
* replacement agent forms its own). System notices are kept: platform
|
|
94
|
+
* messages like budget warnings or degradation notices are turn context the
|
|
95
|
+
* agent acted under.
|
|
96
|
+
*/
|
|
97
|
+
export function composeTurnRecoveryDigest(
|
|
98
|
+
messages: readonly AgentMessage[],
|
|
99
|
+
): string | undefined {
|
|
100
|
+
try {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
for (const message of messages) {
|
|
103
|
+
lines.push(...messageLines(message));
|
|
104
|
+
}
|
|
105
|
+
if (lines.length === 0) return undefined;
|
|
106
|
+
|
|
107
|
+
// Oldest-first; drop from the FRONT when over budget so the newest work
|
|
108
|
+
// always survives, and disclose the drop (DD-013 enforcement shape). The
|
|
109
|
+
// notice participates in the budget so the result never overshoots.
|
|
110
|
+
let first = 0;
|
|
111
|
+
while (first < lines.length && totalLength(lines, first) > MAX_DIGEST_CHARS) {
|
|
112
|
+
first++;
|
|
113
|
+
}
|
|
114
|
+
if (first >= lines.length) {
|
|
115
|
+
// Degenerate case: even the newest line alone busts the budget (it is
|
|
116
|
+
// already line-truncated, so this would take a pathological budget/line
|
|
117
|
+
// ratio) — keep that one line rather than rendering nothing.
|
|
118
|
+
first = lines.length - 1;
|
|
119
|
+
}
|
|
120
|
+
const kept = lines.slice(first);
|
|
121
|
+
if (first > 0) kept.unshift(OMISSION_NOTICE);
|
|
122
|
+
return kept.join("\n");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn("Turn-recovery digest composition failed — recovering without a transcript:", err);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The framed recovery body (preamble + digest), ready for section wrapping.
|
|
131
|
+
* Accepts an absent digest: the state-loss disclosure is load-bearing even
|
|
132
|
+
* when there is no transcript to show (see {@link TURN_RECOVERY_NO_TRANSCRIPT}).
|
|
133
|
+
*/
|
|
134
|
+
export function formatTurnRecoveryText(digest: string | undefined): string {
|
|
135
|
+
if (digest === undefined || digest.trim() === "") {
|
|
136
|
+
return TURN_RECOVERY_NO_TRANSCRIPT;
|
|
137
|
+
}
|
|
138
|
+
return `${TURN_RECOVERY_PREAMBLE}\n\n${digest.trim()}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The digest lines one transcript message contributes (possibly none). */
|
|
142
|
+
function messageLines(message: AgentMessage): string[] {
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
const text = message.content.trim();
|
|
145
|
+
switch (message.type) {
|
|
146
|
+
case MessageType.MESSAGE_AI:
|
|
147
|
+
if (text) lines.push(`Assistant: ${truncate(text, MAX_LINE_CHARS)}`);
|
|
148
|
+
// Tool calls ride AI messages (message.proto contract); render them
|
|
149
|
+
// even when the surrounding text is blank.
|
|
150
|
+
for (const toolCall of message.toolCalls) {
|
|
151
|
+
lines.push(toolCallLine(toolCall));
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
case MessageType.MESSAGE_SYSTEM:
|
|
155
|
+
if (text) lines.push(`System: ${truncate(text, MAX_LINE_CHARS)}`);
|
|
156
|
+
return lines;
|
|
157
|
+
default:
|
|
158
|
+
// Human (already in <user_request>), thinking (model-internal), tool
|
|
159
|
+
// results (each ToolCall line already carries its outcome), unknown.
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One bounded line per tool call: what was attempted and how it ended.
|
|
166
|
+
* Prefers the resolved approval message where one exists — the same
|
|
167
|
+
* human-meaningful description the user approved against
|
|
168
|
+
* (the `describeApproval` convention).
|
|
169
|
+
*/
|
|
170
|
+
function toolCallLine(toolCall: ToolCall): string {
|
|
171
|
+
const action = toolCall.approvalMessage
|
|
172
|
+
? toolCall.approvalMessage
|
|
173
|
+
: toolCall.argsPreview
|
|
174
|
+
? `${toolCall.name}(${truncate(toolCall.argsPreview, MAX_ARGS_CHARS)})`
|
|
175
|
+
: toolCall.name;
|
|
176
|
+
return `Tool: ${truncate(action, MAX_LINE_CHARS)} — ${outcome(toolCall)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function outcome(toolCall: ToolCall): string {
|
|
180
|
+
switch (toolCall.status) {
|
|
181
|
+
case ToolCallStatus.TOOL_CALL_COMPLETED:
|
|
182
|
+
return "completed";
|
|
183
|
+
case ToolCallStatus.TOOL_CALL_FAILED:
|
|
184
|
+
return toolCall.error
|
|
185
|
+
? `failed: ${truncate(toolCall.error, MAX_ARGS_CHARS)}`
|
|
186
|
+
: "failed";
|
|
187
|
+
case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
|
|
188
|
+
return "paused for user approval";
|
|
189
|
+
case ToolCallStatus.TOOL_CALL_SKIPPED:
|
|
190
|
+
return "skipped";
|
|
191
|
+
default:
|
|
192
|
+
// PENDING / RUNNING / unknown: in flight when the session was lost.
|
|
193
|
+
return "interrupted before it finished";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function truncate(text: string, max: number): string {
|
|
198
|
+
return text.length <= max ? text : `${text.slice(0, max)}${ELLIPSIS}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Joined length of lines[first..] plus the omission notice when one would render. */
|
|
202
|
+
function totalLength(lines: readonly string[], first: number): number {
|
|
203
|
+
let total = first > 0 ? OMISSION_NOTICE.length + 1 : 0;
|
|
204
|
+
for (let i = first; i < lines.length; i++) {
|
|
205
|
+
total += lines[i].length + 1;
|
|
206
|
+
}
|
|
207
|
+
return total;
|
|
208
|
+
}
|
|
@@ -1014,3 +1014,108 @@ describe("injectAttachments — vision selection", () => {
|
|
|
1014
1014
|
expect(injected[1].visionDegraded).toBe("budget_exhausted");
|
|
1015
1015
|
});
|
|
1016
1016
|
});
|
|
1017
|
+
|
|
1018
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1019
|
+
// injectAttachments — download-URL hand-off (issue #532)
|
|
1020
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1021
|
+
// The URL is strictly additive (shared/attachment-download-urls.ts): every
|
|
1022
|
+
// case also asserts the file injected exactly as it would without one.
|
|
1023
|
+
|
|
1024
|
+
describe("injectAttachments — download-URL hand-off", () => {
|
|
1025
|
+
it("mints a URL for a storage-key attachment", async () => {
|
|
1026
|
+
const content = Buffer.from("cloud data");
|
|
1027
|
+
const storage = makeMockStorage();
|
|
1028
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1029
|
+
const backend = mockWorkspaceBackend();
|
|
1030
|
+
|
|
1031
|
+
const result = await injectAttachments({
|
|
1032
|
+
backend,
|
|
1033
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1034
|
+
storage,
|
|
1035
|
+
isLocalMode: false,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
expect(result[0]).toMatchObject({
|
|
1039
|
+
path: ".stigmer/inputs/data.csv",
|
|
1040
|
+
downloadUrl: "mem://attachments/xyz/data.csv",
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it("mints a URL for an explicit-mountPath attachment (the rule ignores mount location)", async () => {
|
|
1045
|
+
const storage = makeMockStorage();
|
|
1046
|
+
await storage.upload("attachments/xyz/config.yaml", Buffer.from("a: 1"));
|
|
1047
|
+
const backend = mockWorkspaceBackend();
|
|
1048
|
+
|
|
1049
|
+
const result = await injectAttachments({
|
|
1050
|
+
backend,
|
|
1051
|
+
attachments: [makeAttachment({
|
|
1052
|
+
filename: "config.yaml",
|
|
1053
|
+
storageKey: "attachments/xyz/config.yaml",
|
|
1054
|
+
mountPath: "config/app.yaml",
|
|
1055
|
+
})],
|
|
1056
|
+
storage,
|
|
1057
|
+
isLocalMode: false,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
expect(result[0]).toMatchObject({
|
|
1061
|
+
path: "config/app.yaml",
|
|
1062
|
+
downloadUrl: "mem://attachments/xyz/config.yaml",
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("carries no URL on extracted ZIP entries — the stored object is the ZIP, not any listed file", async () => {
|
|
1067
|
+
const zip = makeZip({ "src/a.txt": "alpha", "src/b.txt": "beta" });
|
|
1068
|
+
const storage = makeMockStorage();
|
|
1069
|
+
await storage.upload("attachments/xyz/code.zip", zip);
|
|
1070
|
+
const backend = mockWorkspaceBackend();
|
|
1071
|
+
|
|
1072
|
+
const result = await injectAttachments({
|
|
1073
|
+
backend,
|
|
1074
|
+
attachments: [makeAttachment({
|
|
1075
|
+
filename: "code.zip",
|
|
1076
|
+
storageKey: "attachments/xyz/code.zip",
|
|
1077
|
+
extract: true,
|
|
1078
|
+
})],
|
|
1079
|
+
storage,
|
|
1080
|
+
isLocalMode: false,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
expect(result.length).toBeGreaterThan(0);
|
|
1084
|
+
for (const file of result) {
|
|
1085
|
+
expect(file.downloadUrl).toBeUndefined();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("carries no URL for a key-less local file", async () => {
|
|
1090
|
+
const localFile = join(tmpdir(), `injector-url-${Date.now()}.txt`);
|
|
1091
|
+
await writeFile(localFile, "local only");
|
|
1092
|
+
const backend = mockWorkspaceBackend();
|
|
1093
|
+
|
|
1094
|
+
const result = await injectAttachments({
|
|
1095
|
+
backend,
|
|
1096
|
+
attachments: [makeAttachment({ filename: "local.txt", storageKey: "", localPath: localFile })],
|
|
1097
|
+
storage: makeMockStorage(),
|
|
1098
|
+
isLocalMode: true,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("degrades to no URL when the mint fails — file still injected, no throw", async () => {
|
|
1105
|
+
const content = Buffer.from("cloud data");
|
|
1106
|
+
const storage = makeMockStorage();
|
|
1107
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1108
|
+
storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
|
|
1109
|
+
const backend = mockWorkspaceBackend();
|
|
1110
|
+
|
|
1111
|
+
const result = await injectAttachments({
|
|
1112
|
+
backend,
|
|
1113
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1114
|
+
storage,
|
|
1115
|
+
isLocalMode: false,
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1119
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/data.csv", content);
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
@@ -184,7 +184,6 @@ function fakeHttpRejectSetup(): SetupResult {
|
|
|
184
184
|
session: { spec: { workspaceEntries: [] } },
|
|
185
185
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-http" },
|
|
186
186
|
mcpConnection: null,
|
|
187
|
-
datastoreToolsMissing: [],
|
|
188
187
|
mergedEnvVars: {},
|
|
189
188
|
secretKeys: new Set<string>(),
|
|
190
189
|
modelName: "claude-sonnet",
|
|
@@ -220,7 +219,6 @@ function fakeMemoryRejectSetup(): SetupResult {
|
|
|
220
219
|
session: { spec: { workspaceEntries: [] } },
|
|
221
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-mem" },
|
|
222
221
|
mcpConnection: null,
|
|
223
|
-
datastoreToolsMissing: [],
|
|
224
222
|
mergedEnvVars: {},
|
|
225
223
|
secretKeys: new Set<string>(),
|
|
226
224
|
modelName: "claude-sonnet",
|
|
@@ -264,7 +264,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
264
264
|
// path shared across files would serialize parallel test workers for real.
|
|
265
265
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-approve-all" },
|
|
266
266
|
mcpConnection: null,
|
|
267
|
-
datastoreToolsMissing: [],
|
|
268
267
|
mergedEnvVars: {},
|
|
269
268
|
secretKeys: new Set<string>(),
|
|
270
269
|
modelName: "claude-sonnet",
|
|
@@ -219,7 +219,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
219
219
|
// path shared across files would serialize parallel test workers for real.
|
|
220
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-history" },
|
|
221
221
|
mcpConnection: null,
|
|
222
|
-
datastoreToolsMissing: [],
|
|
223
222
|
mergedEnvVars: {},
|
|
224
223
|
secretKeys: new Set<string>(),
|
|
225
224
|
modelName: "claude-sonnet",
|
|
@@ -12,7 +12,6 @@ import { shouldConnectMcp } from "../mcp-gate.js";
|
|
|
12
12
|
|
|
13
13
|
const nothing = {
|
|
14
14
|
mcpServerUsageCount: 0,
|
|
15
|
-
datastoreUsageCount: 0,
|
|
16
15
|
channelMessagingCount: 0,
|
|
17
16
|
conversationChannelId: undefined,
|
|
18
17
|
};
|
|
@@ -26,17 +25,13 @@ describe("shouldConnectMcp", () => {
|
|
|
26
25
|
expect(shouldConnectMcp({ ...nothing, mcpServerUsageCount: 1 })).toBe(true);
|
|
27
26
|
});
|
|
28
27
|
|
|
29
|
-
it("enters on datastore usages alone (the records attachment)", () => {
|
|
30
|
-
expect(shouldConnectMcp({ ...nothing, datastoreUsageCount: 1 })).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
28
|
it("enters on a serving proactive channel alone (the channels attachment)", () => {
|
|
34
29
|
expect(shouldConnectMcp({ ...nothing, channelMessagingCount: 1 })).toBe(true);
|
|
35
30
|
});
|
|
36
31
|
|
|
37
32
|
it("enters on a channel conversation alone (the conversation attachment)", () => {
|
|
38
|
-
// The reply-only pilot shape: no declared servers, no
|
|
39
|
-
//
|
|
33
|
+
// The reply-only pilot shape: no declared servers, no proactive
|
|
34
|
+
// channel — the escalation tool is the ONLY source.
|
|
40
35
|
expect(shouldConnectMcp({ ...nothing, conversationChannelId: "agch_1" })).toBe(true);
|
|
41
36
|
});
|
|
42
37
|
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that
|
|
3
|
+
* a plan-mode PARENT graph accepts workspace-relative paths (issue #429).
|
|
4
|
+
*
|
|
5
|
+
* Before the fix, deepagents' permission enforcement canonicalized every
|
|
6
|
+
* filesystem tool-call path BEFORE any rule ran and refused non-absolute
|
|
7
|
+
* shapes, so on a rule-bearing graph a workspace-relative call — reads
|
|
8
|
+
* included — died with `path must be absolute` instead of just working. The
|
|
9
|
+
* path-normalization middleware (middleware/path-normalization.ts) rewrites
|
|
10
|
+
* relative paths to workspace-absolute at our seam, before enforcement sees
|
|
11
|
+
* them.
|
|
12
|
+
*
|
|
13
|
+
* The graph here is composed exactly the way setup.ts composes the parent:
|
|
14
|
+
* the PRODUCTION buildMiddlewareStack (pathNormalization present, the
|
|
15
|
+
* rule-bearing shape) + the CAS capture backend + the PRODUCTION
|
|
16
|
+
* PLAN_MODE_PERMISSIONS constant. These tests are also the empirical proof
|
|
17
|
+
* that langchain's wrapToolCall seam delivers rewritten args to the tool —
|
|
18
|
+
* if it did not, the relative read below could never succeed.
|
|
19
|
+
*
|
|
20
|
+
* The sub-agent twin of this contract is pinned in
|
|
21
|
+
* subagent-plan-mode-permissions.test.ts (issue #255 wiring).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
25
|
+
import { mkdtemp, mkdir, rm, readFile, writeFile, access } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { HumanMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
|
|
29
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
30
|
+
import { createDeepAgent } from "deepagents";
|
|
31
|
+
|
|
32
|
+
import { PLAN_MODE_PERMISSIONS } from "../../../shared/plan-mode-permissions.js";
|
|
33
|
+
import { buildMiddlewareStack } from "../../../middleware/index.js";
|
|
34
|
+
import { createCasCaptureBackend } from "../cas-capture-backend.js";
|
|
35
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
36
|
+
import { ScriptedModel, type ScriptSelector } from "../__test-utils__/scripted-model.js";
|
|
37
|
+
|
|
38
|
+
const SEEDED_CONTENT = "PLAN_MODE_README_TOKEN: hello from the seeded file";
|
|
39
|
+
|
|
40
|
+
describe("plan-mode parent path normalization (issue #429)", () => {
|
|
41
|
+
let root: string;
|
|
42
|
+
let observer: CasCaptureObserver;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
root = await mkdtemp(join(tmpdir(), "plan-path-"));
|
|
46
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
47
|
+
await writeFile(join(root, "src/notes.md"), SEEDED_CONTENT);
|
|
48
|
+
observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a parent graph the way setup.ts does in plan mode: the production
|
|
57
|
+
* middleware stack with pathNormalization set (derived, like the graph's
|
|
58
|
+
* permissions, from the plan-mode rules), a filesystem-only CAS capture
|
|
59
|
+
* backend (no shellEnv — plan mode clears it), and PLAN_MODE_PERMISSIONS
|
|
60
|
+
* on the graph.
|
|
61
|
+
*/
|
|
62
|
+
async function buildPlanModeParent(script: ScriptSelector) {
|
|
63
|
+
const { middleware } = buildMiddlewareStack({
|
|
64
|
+
approvalGate: null,
|
|
65
|
+
pathNormalization: { rootDir: root },
|
|
66
|
+
});
|
|
67
|
+
const backend = await createCasCaptureBackend({ rootDir: root, observer });
|
|
68
|
+
return createDeepAgent({
|
|
69
|
+
model: new ScriptedModel(script),
|
|
70
|
+
checkpointer: new MemorySaver() as never,
|
|
71
|
+
backend,
|
|
72
|
+
middleware: middleware as never[],
|
|
73
|
+
permissions: PLAN_MODE_PERMISSIONS,
|
|
74
|
+
} as Parameters<typeof createDeepAgent>[0]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolResultById(messages: BaseMessage[], toolCallId: string): string {
|
|
78
|
+
const match = messages.find(
|
|
79
|
+
(m): m is ToolMessage => m instanceof ToolMessage && m.tool_call_id === toolCallId,
|
|
80
|
+
);
|
|
81
|
+
expect(match, `expected a tool result for call '${toolCallId}'`).toBeDefined();
|
|
82
|
+
return typeof match!.content === "string" ? match!.content : JSON.stringify(match!.content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function invokeOnce(script: ScriptSelector, threadId: string) {
|
|
86
|
+
const agent = await buildPlanModeParent(script);
|
|
87
|
+
return (await agent.invoke(
|
|
88
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
89
|
+
{ configurable: { thread_id: threadId }, recursionLimit: 50 },
|
|
90
|
+
)) as { messages: BaseMessage[] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it("a prompt-compliant relative read succeeds on the first tool round", async () => {
|
|
94
|
+
const result = await invokeOnce(
|
|
95
|
+
() => ({
|
|
96
|
+
toolCalls: [
|
|
97
|
+
{ name: "read_file", args: { file_path: "src/notes.md" }, id: "c_read_rel" },
|
|
98
|
+
],
|
|
99
|
+
done: "done",
|
|
100
|
+
}),
|
|
101
|
+
"t_read_rel",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const readResult = toolResultById(result.messages, "c_read_rel");
|
|
105
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
106
|
+
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
107
|
+
expect(readResult).not.toMatch(/permission denied/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("a relative write is refused by the RULES — permission denied, not a path-shape error", async () => {
|
|
111
|
+
const result = await invokeOnce(
|
|
112
|
+
() => ({
|
|
113
|
+
toolCalls: [
|
|
114
|
+
{ name: "write_file", args: { file_path: "src/out.txt", content: "x" }, id: "c_write_rel" },
|
|
115
|
+
],
|
|
116
|
+
done: "done",
|
|
117
|
+
}),
|
|
118
|
+
"t_write_rel",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// The deny-all-writes rule fires on the normalized path — the honest
|
|
122
|
+
// plan-mode refusal the model can reason about, instead of the
|
|
123
|
+
// pre-fix validation error that taught it to abandon relative paths.
|
|
124
|
+
expect(toolResultById(result.messages, "c_write_rel")).toMatch(/permission denied for write/i);
|
|
125
|
+
expect(toolResultById(result.messages, "c_write_rel")).not.toMatch(/path must be absolute/i);
|
|
126
|
+
await expect(access(join(root, "src/out.txt"))).rejects.toThrow();
|
|
127
|
+
expect(observer.before.size).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("an escaping relative read is still refused — normalization grants no new reachability", async () => {
|
|
131
|
+
// Outside the root, reachable only if `..` were naively joined away.
|
|
132
|
+
const result = await invokeOnce(
|
|
133
|
+
() => ({
|
|
134
|
+
toolCalls: [
|
|
135
|
+
{ name: "read_file", args: { file_path: "../escape.txt" }, id: "c_read_escape" },
|
|
136
|
+
],
|
|
137
|
+
done: "done",
|
|
138
|
+
}),
|
|
139
|
+
"t_read_escape",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Left raw by the middleware, refused by upstream validation exactly as
|
|
143
|
+
// before the fix.
|
|
144
|
+
expect(toolResultById(result.messages, "c_read_escape")).toMatch(/must not contain|path must be absolute/i);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("absolute in-workspace paths keep working unchanged", async () => {
|
|
148
|
+
const result = await invokeOnce(
|
|
149
|
+
() => ({
|
|
150
|
+
toolCalls: [
|
|
151
|
+
{ name: "read_file", args: { file_path: join(root, "src/notes.md") }, id: "c_read_abs" },
|
|
152
|
+
],
|
|
153
|
+
done: "done",
|
|
154
|
+
}),
|
|
155
|
+
"t_read_abs",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(toolResultById(result.messages, "c_read_abs")).toContain("PLAN_MODE_README_TOKEN");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("relative ls flows through normalization to list the workspace directory", async () => {
|
|
162
|
+
const result = await invokeOnce(
|
|
163
|
+
() => ({
|
|
164
|
+
toolCalls: [{ name: "ls", args: { path: "src" }, id: "c_ls_rel" }],
|
|
165
|
+
done: "done",
|
|
166
|
+
}),
|
|
167
|
+
"t_ls_rel",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const lsResult = toolResultById(result.messages, "c_ls_rel");
|
|
171
|
+
expect(lsResult).toContain("notes.md");
|
|
172
|
+
expect(lsResult).not.toMatch(/path must be absolute/i);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -190,6 +190,77 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
190
190
|
expect(prompt).toContain("`.stigmer/inputs/report.pdf` (10 bytes)\n");
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it("lists a minted download URL beside its file with the presigned hand-off line (issue #532)", () => {
|
|
194
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
195
|
+
instructions: "Test",
|
|
196
|
+
provisionResults: [],
|
|
197
|
+
containerRoot: "",
|
|
198
|
+
skillsPromptSection: "",
|
|
199
|
+
workspaceFileRefs: [],
|
|
200
|
+
workspaceRoot: "/workspace",
|
|
201
|
+
injectedFiles: [
|
|
202
|
+
{
|
|
203
|
+
filename: "lease.pdf",
|
|
204
|
+
path: ".stigmer/inputs/lease.pdf",
|
|
205
|
+
sizeBytes: 2048,
|
|
206
|
+
downloadUrl: "https://r2.example/lease?sig=abc",
|
|
207
|
+
},
|
|
208
|
+
{ filename: "notes.md", path: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
209
|
+
],
|
|
210
|
+
downloadUrlKind: "presigned",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(prompt).toContain(
|
|
214
|
+
"`.stigmer/inputs/lease.pdf` (2048 bytes) — download URL: https://r2.example/lease?sig=abc",
|
|
215
|
+
);
|
|
216
|
+
// The URL-less file keeps a clean entry.
|
|
217
|
+
expect(prompt).toContain("`.stigmer/inputs/notes.md` (12 bytes)\n");
|
|
218
|
+
expect(prompt).toContain("These URLs are time-limited");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("words a local-serve URL honestly — reachable only from this machine", () => {
|
|
222
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
223
|
+
instructions: "Test",
|
|
224
|
+
provisionResults: [],
|
|
225
|
+
containerRoot: "",
|
|
226
|
+
skillsPromptSection: "",
|
|
227
|
+
workspaceFileRefs: [],
|
|
228
|
+
workspaceRoot: "/workspace",
|
|
229
|
+
injectedFiles: [
|
|
230
|
+
{
|
|
231
|
+
filename: "lease.pdf",
|
|
232
|
+
path: ".stigmer/inputs/lease.pdf",
|
|
233
|
+
sizeBytes: 2048,
|
|
234
|
+
downloadUrl: "http://localhost:7235/attachments/01A/lease.pdf",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
downloadUrlKind: "local-serve",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(prompt).toContain("download URL: http://localhost:7235/attachments/01A/lease.pdf");
|
|
241
|
+
expect(prompt).toContain("reachable only from this machine");
|
|
242
|
+
expect(prompt).not.toContain("time-limited");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders no hand-off line when no listed file carries a URL (kind alone is not enough)", () => {
|
|
246
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
247
|
+
instructions: "Test",
|
|
248
|
+
provisionResults: [],
|
|
249
|
+
containerRoot: "",
|
|
250
|
+
skillsPromptSection: "",
|
|
251
|
+
workspaceFileRefs: [],
|
|
252
|
+
workspaceRoot: "/workspace",
|
|
253
|
+
injectedFiles: [
|
|
254
|
+
{ filename: "local.csv", path: ".stigmer/inputs/local.csv", sizeBytes: 5 },
|
|
255
|
+
],
|
|
256
|
+
downloadUrlKind: "presigned",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(prompt).toContain("## Input Files");
|
|
260
|
+
expect(prompt).not.toContain("download URL");
|
|
261
|
+
expect(prompt).not.toContain("time-limited");
|
|
262
|
+
});
|
|
263
|
+
|
|
193
264
|
it("produces a git repo description for git workspace entries", () => {
|
|
194
265
|
const prompt = buildEnhancedSystemPrompt({
|
|
195
266
|
instructions: "Test",
|