@app-vitals/agent-runtime 0.0.1 → 0.1.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/Dockerfile +265 -0
- package/agent-account-status-ref.ts +53 -0
- package/agent-account-status-ref.unit.test.ts +24 -0
- package/agent-extension.ts +144 -0
- package/agent-extension.unit.test.ts +139 -0
- package/agent-repos-ref.ts +63 -0
- package/agent-repos-ref.unit.test.ts +84 -0
- package/agent-slack-membership-ref.ts +94 -0
- package/agent-slack-membership-ref.unit.test.ts +201 -0
- package/agent-trial-expiry-ref.ts +63 -0
- package/agent-trial-expiry-ref.unit.test.ts +84 -0
- package/anthropic-federation-guard.ts +72 -0
- package/anthropic-federation-guard.unit.test.ts +127 -0
- package/base-image-contract.smoke.test.ts +149 -0
- package/boot.ts +845 -0
- package/browser.integration.test.ts +52 -0
- package/browser.ts +34 -0
- package/chat-poller.integration.test.ts +109 -0
- package/chat-poller.ts +401 -0
- package/chat-poller.unit.test.ts +1581 -0
- package/chat-token-reporter.integration.test.ts +282 -0
- package/chat-token-reporter.ts +136 -0
- package/claude-version.ts +55 -0
- package/claude-version.unit.test.ts +73 -0
- package/claude.ts +1054 -0
- package/claude.unit.test.ts +3042 -0
- package/cleanup-after-days-ref.ts +83 -0
- package/cleanup-after-days-ref.unit.test.ts +89 -0
- package/cleanup-merged-worktrees-ref.ts +75 -0
- package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
- package/cli-args.ts +36 -0
- package/cli-args.unit.test.ts +94 -0
- package/clock.ts +47 -0
- package/config.ts +99 -0
- package/config.unit.test.ts +356 -0
- package/context-stamp.ts +189 -0
- package/context-stamp.unit.test.ts +159 -0
- package/cron-failure-reporter.ts +107 -0
- package/cron-failure-reporter.unit.test.ts +244 -0
- package/cron-handler.integration.test.ts +614 -0
- package/cron-handler.smoke.test.ts +1371 -0
- package/cron-handler.ts +750 -0
- package/cron-handler.unit.test.ts +1177 -0
- package/cron-run-reporter.integration.test.ts +806 -0
- package/cron-run-reporter.ts +398 -0
- package/crypto.ts +73 -0
- package/cutover-validate.integration.test.ts +245 -0
- package/cutover-validate.ts +84 -0
- package/entrypoint-main.ts +79 -0
- package/entrypoint.integration.test.ts +315 -0
- package/entrypoint.ts +154 -0
- package/errors.ts +61 -0
- package/fixtures/chat-service/attachment.json +15 -0
- package/fixtures/chat-service/messages.json +86 -0
- package/fixtures/chat-service/threads.json +50 -0
- package/fixtures/github-installations/installations.json +1231 -0
- package/fixtures/slack-provision-cassette.json +6 -0
- package/fixtures/specialties/fixture/manifest.yaml +35 -0
- package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
- package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
- package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
- package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
- package/fixtures/stream-json/clean-multi-turn.ts +209 -0
- package/fixtures/stream-json/progress-phases.ts +203 -0
- package/fixtures/stream-json/skill-attribution.ts +251 -0
- package/fixtures/stream-json/truncated-no-result.ts +84 -0
- package/format.ts +283 -0
- package/format.unit.test.ts +558 -0
- package/gh-helpers.ts +139 -0
- package/gh-helpers.unit.test.ts +253 -0
- package/gh-token-files.integration.test.ts +143 -0
- package/gh-token-files.ts +92 -0
- package/gh-token-files.unit.test.ts +63 -0
- package/github-app-auth.integration.test.ts +419 -0
- package/github-app-auth.ts +193 -0
- package/github-auth-deps.ts +128 -0
- package/github-auth-startup.ts +231 -0
- package/github-auth-startup.unit.test.ts +325 -0
- package/github-installations-manager.integration.test.ts +323 -0
- package/github-installations-reporter.integration.test.ts +124 -0
- package/github-installations-reporter.ts +150 -0
- package/github-installations-reporter.unit.test.ts +166 -0
- package/github-installations.integration.test.ts +88 -0
- package/github-installations.ts +462 -0
- package/github-installations.unit.test.ts +98 -0
- package/health.smoke.test.ts +244 -0
- package/health.ts +193 -0
- package/health.unit.test.ts +40 -0
- package/http-chat-service-client.integration.test.ts +345 -0
- package/http-chat-service-client.ts +287 -0
- package/http-chat-service-client.unit.test.ts +500 -0
- package/log-prefix.ts +12 -0
- package/log-prefix.unit.test.ts +24 -0
- package/markers.ts +178 -0
- package/markers.unit.test.ts +374 -0
- package/package.json +38 -4
- package/piper-voice.ts +116 -0
- package/piper-voice.unit.test.ts +124 -0
- package/pricing.ts +7 -0
- package/pricing.unit.test.ts +149 -0
- package/process-tree-kill.integration.test.ts +133 -0
- package/process-tree-kill.ts +325 -0
- package/process-tree-kill.unit.test.ts +265 -0
- package/progress-milestones.ts +86 -0
- package/progress-milestones.unit.test.ts +188 -0
- package/run-telemetry.ts +241 -0
- package/run-telemetry.unit.test.ts +235 -0
- package/sessions.integration.test.ts +273 -0
- package/sessions.ts +191 -0
- package/sessions.unit.test.ts +94 -0
- package/setup-github-auth.integration.test.ts +858 -0
- package/setup-github-auth.ts +297 -0
- package/setup.fixture.integration.test.ts +109 -0
- package/setup.integration.test.ts +1495 -0
- package/setup.ts +853 -0
- package/setup.unit.test.ts +286 -0
- package/shipwright-config-client.ts +36 -0
- package/shipwright-runtime-client.integration.test.ts +336 -0
- package/shipwright-runtime-client.ts +118 -0
- package/slack-account-status.unit.test.ts +251 -0
- package/slack-bolt-client.integration.test.ts +76 -0
- package/slack-chat-conversation.integration.test.ts +333 -0
- package/slack-progress.ts +510 -0
- package/slack-progress.unit.test.ts +806 -0
- package/slack-socket-mode-ping.integration.test.ts +137 -0
- package/slack-startup.ts +170 -0
- package/slack-startup.unit.test.ts +300 -0
- package/slack-thread-status-tracker.ts +70 -0
- package/slack-thread-status-tracker.unit.test.ts +118 -0
- package/slack-trial-expiry.unit.test.ts +269 -0
- package/slack.integration.test.ts +3979 -0
- package/slack.ts +1286 -0
- package/slack.unit.test.ts +1070 -0
- package/spawn-env.ts +58 -0
- package/spawn-env.unit.test.ts +96 -0
- package/startup-dm.integration.test.ts +156 -0
- package/startup-dm.ts +29 -0
- package/sync-config-clone.ts +238 -0
- package/sync-config-clone.unit.test.ts +429 -0
- package/test-env.ts +27 -0
- package/users.ts +90 -0
- package/users.unit.test.ts +108 -0
- package/voice.integration.test.ts +669 -0
- package/voice.ts +315 -0
- package/voice.unit.test.ts +105 -0
- package/workspace/BOOTSTRAP.md.template +43 -0
- package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
- package/workspace/IDENTITY.md.template +8 -0
- package/workspace/RUNTIME.md.template +193 -0
- package/workspace/RUNTIME.md.template.content.test.ts +75 -0
- package/workspace/SOUL.md.template +45 -0
- package/workspace/SPECIALTY.md.template +37 -0
- package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
- package/workspace/VOICE.md.template +8 -0
- package/workspace/mise.toml.template +12 -0
- package/workspace/requirements.txt.template +1 -0
- package/workspace-paths.ts +37 -0
- package/worktree-reaper.ts +244 -0
- package/worktree-reaper.unit.test.ts +380 -0
- package/README.md +0 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Minimal fixture specialty (AGR-1.9). Platform tests point
|
|
2
|
+
# SHIPWRIGHT_SPECIALTIES_DIR at the parent of this directory and
|
|
3
|
+
# SHIPWRIGHT_DEFAULT_AGENT_TYPE at "fixture", so the platform test suite needs
|
|
4
|
+
# no coding specialty. It is test data, not a product specialty: no crons, no
|
|
5
|
+
# plugins, no GitHub scope.
|
|
6
|
+
apiVersion: shipwright.dev/v1alpha1
|
|
7
|
+
kind: Specialty
|
|
8
|
+
metadata:
|
|
9
|
+
name: fixture
|
|
10
|
+
displayName: Fixture Agent
|
|
11
|
+
description: Minimal specialty used by the platform test suite.
|
|
12
|
+
version: 1.0.0
|
|
13
|
+
skills: []
|
|
14
|
+
identity:
|
|
15
|
+
# Resolved against the repo root first, then against this manifest's own directory.
|
|
16
|
+
templatesDir: templates/
|
|
17
|
+
crons: []
|
|
18
|
+
plugins:
|
|
19
|
+
- fixture-plugin
|
|
20
|
+
tools:
|
|
21
|
+
- Bash
|
|
22
|
+
- WebSearch
|
|
23
|
+
env:
|
|
24
|
+
required:
|
|
25
|
+
- key: CLAUDE_CODE_OAUTH_TOKEN
|
|
26
|
+
description: Claude Code OAuth token used to authenticate the agent's Claude sessions.
|
|
27
|
+
secret: true
|
|
28
|
+
optional:
|
|
29
|
+
- key: GH_TOKEN
|
|
30
|
+
description: GitHub Personal Access Token.
|
|
31
|
+
secret: true
|
|
32
|
+
members: []
|
|
33
|
+
repos: []
|
|
34
|
+
chat: true
|
|
35
|
+
voice: true
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SOUL.md — Who You Are
|
|
2
|
+
|
|
3
|
+
_You're not a chatbot. You're becoming someone._
|
|
4
|
+
|
|
5
|
+
## Core Truths
|
|
6
|
+
|
|
7
|
+
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and
|
|
8
|
+
"I'd be happy to help!" — just help. Actions speak louder than filler words.
|
|
9
|
+
|
|
10
|
+
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or
|
|
11
|
+
boring. An assistant with no personality is just a search engine with extra steps.
|
|
12
|
+
|
|
13
|
+
**Be resourceful before asking.** Try to figure it out. Read the file. Check the
|
|
14
|
+
context. Search for it. _Then_ ask if you're stuck. The goal is to come back with
|
|
15
|
+
answers, not questions.
|
|
16
|
+
|
|
17
|
+
**Earn trust through competence.** Your human gave you access to their stuff. Don't
|
|
18
|
+
make them regret it. Be careful with external actions (emails, posts, anything
|
|
19
|
+
public). Be bold with internal ones (reading, organizing, learning).
|
|
20
|
+
|
|
21
|
+
**Remember you're a guest.** You have access to someone's life — their messages,
|
|
22
|
+
files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
|
23
|
+
|
|
24
|
+
## Boundaries
|
|
25
|
+
|
|
26
|
+
- Private things stay private. Period.
|
|
27
|
+
- When in doubt, ask before acting externally.
|
|
28
|
+
- Never send half-baked replies to messaging surfaces.
|
|
29
|
+
- You're not the user's voice — be careful in group chats.
|
|
30
|
+
|
|
31
|
+
## Vibe
|
|
32
|
+
|
|
33
|
+
Be the assistant you'd actually want to talk to. Concise when needed, thorough when
|
|
34
|
+
it matters. Not a corporate drone. Not a sycophant. Just... good.
|
|
35
|
+
|
|
36
|
+
## Continuity
|
|
37
|
+
|
|
38
|
+
Each session, you wake up fresh. These files _are_ your memory. Read them. Update
|
|
39
|
+
them. They're how you persist.
|
|
40
|
+
|
|
41
|
+
If you change this file, tell the user — it's your soul, and they should know.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
_This file is yours to evolve. As you learn who you are, update it._
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored synthetic `claude -p --output-format stream-json --verbose`
|
|
3
|
+
* transcript: the process exits CLEANLY (exit code 0) but the stream ends
|
|
4
|
+
* without ever emitting a terminal `result` line — e.g. the CLI closed stdout
|
|
5
|
+
* right after its last turn without writing the final summary line.
|
|
6
|
+
*
|
|
7
|
+
* Distinct from truncated-no-result.ts (which pairs with a non-zero exit
|
|
8
|
+
* code): this fixture exercises the `streamIncomplete: true` clean-exit return
|
|
9
|
+
* path in `_spawn`, which must still surface the `session_id` captured off
|
|
10
|
+
* the leading `system`/`init` line even though no terminal `result` event
|
|
11
|
+
* ever carried one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SONNET = "claude-sonnet-4-6";
|
|
15
|
+
const SESSION_ID = "sess-clean-exit-no-result";
|
|
16
|
+
|
|
17
|
+
export const lines: string[] = [
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
type: "system",
|
|
20
|
+
subtype: "init",
|
|
21
|
+
session_id: SESSION_ID,
|
|
22
|
+
model: SONNET,
|
|
23
|
+
}),
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
type: "assistant",
|
|
26
|
+
message: {
|
|
27
|
+
id: "msg_1",
|
|
28
|
+
role: "assistant",
|
|
29
|
+
model: SONNET,
|
|
30
|
+
content: [{ type: "text", text: "Working on it..." }],
|
|
31
|
+
usage: {
|
|
32
|
+
input_tokens: 90,
|
|
33
|
+
output_tokens: 30,
|
|
34
|
+
cache_read_input_tokens: 0,
|
|
35
|
+
cache_creation_input_tokens: 0,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
session_id: SESSION_ID,
|
|
39
|
+
}),
|
|
40
|
+
// stream closes cleanly here — no `result` line
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const expectedAccumulated = {
|
|
44
|
+
[SONNET]: {
|
|
45
|
+
inputTokens: 90,
|
|
46
|
+
outputTokens: 30,
|
|
47
|
+
cacheReadInputTokens: 0,
|
|
48
|
+
cacheCreationInputTokens: 0,
|
|
49
|
+
costUSD: 0,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const expectedSessionId = SESSION_ID;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored synthetic `claude -p --output-format stream-json --verbose`
|
|
3
|
+
* transcript: a clean multi-turn, multi-model session that ends in a `result`
|
|
4
|
+
* event.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: These NDJSON lines are hand-authored per the documented public Claude
|
|
7
|
+
* Code CLI stream-json/--verbose schema — they stand in for real captures that
|
|
8
|
+
* will be swapped in and validated later. Each entry in `lines` is one JSON
|
|
9
|
+
* object, exactly as the CLI emits one-per-line on stdout.
|
|
10
|
+
*
|
|
11
|
+
* Shape of the session:
|
|
12
|
+
* - one `system`/`init` line
|
|
13
|
+
* - assistant turn `msg_1` (sonnet) emitted TWICE with identical repeated usage
|
|
14
|
+
* (same message.id spanning two stream lines) — must be deduped to one turn
|
|
15
|
+
* - a `user` tool-result line (no usage)
|
|
16
|
+
* - assistant turn `msg_2` (sonnet)
|
|
17
|
+
* - assistant turn `msg_3` (opus) — a second, different model
|
|
18
|
+
* - one terminal `result`/success line carrying the authoritative
|
|
19
|
+
* modelUsage/total_cost_usd/usage (byte-identical in shape to the old
|
|
20
|
+
* single-blob `--output-format json` output)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SONNET = "claude-sonnet-4-6";
|
|
24
|
+
const OPUS = "claude-opus-4-8";
|
|
25
|
+
const SESSION_ID = "sess-clean-multi";
|
|
26
|
+
|
|
27
|
+
export const lines: string[] = [
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: "system",
|
|
30
|
+
subtype: "init",
|
|
31
|
+
session_id: SESSION_ID,
|
|
32
|
+
model: SONNET,
|
|
33
|
+
tools: ["Read", "Write", "Bash"],
|
|
34
|
+
}),
|
|
35
|
+
// msg_1 — first stream line for this turn
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
type: "assistant",
|
|
38
|
+
message: {
|
|
39
|
+
id: "msg_1",
|
|
40
|
+
role: "assistant",
|
|
41
|
+
model: SONNET,
|
|
42
|
+
content: [{ type: "text", text: "Let me look into that." }],
|
|
43
|
+
usage: {
|
|
44
|
+
input_tokens: 100,
|
|
45
|
+
output_tokens: 50,
|
|
46
|
+
cache_read_input_tokens: 10,
|
|
47
|
+
cache_creation_input_tokens: 5,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
session_id: SESSION_ID,
|
|
51
|
+
}),
|
|
52
|
+
// msg_1 — SAME message.id repeated (e.g. a tool-use block within the turn),
|
|
53
|
+
// identical usage numbers — must NOT be double counted
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
type: "assistant",
|
|
56
|
+
message: {
|
|
57
|
+
id: "msg_1",
|
|
58
|
+
role: "assistant",
|
|
59
|
+
model: SONNET,
|
|
60
|
+
content: [{ type: "tool_use", id: "tu_1", name: "Read", input: {} }],
|
|
61
|
+
usage: {
|
|
62
|
+
input_tokens: 100,
|
|
63
|
+
output_tokens: 50,
|
|
64
|
+
cache_read_input_tokens: 10,
|
|
65
|
+
cache_creation_input_tokens: 5,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
session_id: SESSION_ID,
|
|
69
|
+
}),
|
|
70
|
+
// tool-result feedback — no usage to accumulate
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
type: "user",
|
|
73
|
+
message: {
|
|
74
|
+
role: "user",
|
|
75
|
+
content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }],
|
|
76
|
+
},
|
|
77
|
+
session_id: SESSION_ID,
|
|
78
|
+
}),
|
|
79
|
+
// msg_2 — same model, distinct turn
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
type: "assistant",
|
|
82
|
+
message: {
|
|
83
|
+
id: "msg_2",
|
|
84
|
+
role: "assistant",
|
|
85
|
+
model: SONNET,
|
|
86
|
+
content: [{ type: "text", text: "Now switching models." }],
|
|
87
|
+
usage: {
|
|
88
|
+
input_tokens: 200,
|
|
89
|
+
output_tokens: 80,
|
|
90
|
+
cache_read_input_tokens: 20,
|
|
91
|
+
cache_creation_input_tokens: 0,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
session_id: SESSION_ID,
|
|
95
|
+
}),
|
|
96
|
+
// msg_3 — second, different model
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
type: "assistant",
|
|
99
|
+
message: {
|
|
100
|
+
id: "msg_3",
|
|
101
|
+
role: "assistant",
|
|
102
|
+
model: OPUS,
|
|
103
|
+
content: [{ type: "text", text: "Final answer" }],
|
|
104
|
+
usage: {
|
|
105
|
+
input_tokens: 300,
|
|
106
|
+
output_tokens: 150,
|
|
107
|
+
cache_read_input_tokens: 0,
|
|
108
|
+
cache_creation_input_tokens: 30,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
session_id: SESSION_ID,
|
|
112
|
+
}),
|
|
113
|
+
// terminal result — authoritative totals (costUSD is only known here)
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
type: "result",
|
|
116
|
+
subtype: "success",
|
|
117
|
+
is_error: false,
|
|
118
|
+
result: "Final answer",
|
|
119
|
+
session_id: SESSION_ID,
|
|
120
|
+
usage: {
|
|
121
|
+
input_tokens: 600,
|
|
122
|
+
output_tokens: 280,
|
|
123
|
+
cache_read_input_tokens: 30,
|
|
124
|
+
cache_creation_input_tokens: 35,
|
|
125
|
+
},
|
|
126
|
+
total_cost_usd: 0.0579,
|
|
127
|
+
modelUsage: {
|
|
128
|
+
[SONNET]: {
|
|
129
|
+
inputTokens: 300,
|
|
130
|
+
outputTokens: 130,
|
|
131
|
+
cacheReadInputTokens: 30,
|
|
132
|
+
cacheCreationInputTokens: 5,
|
|
133
|
+
costUSD: 0.0123,
|
|
134
|
+
webSearchRequests: 0,
|
|
135
|
+
contextWindow: 200000,
|
|
136
|
+
maxOutputTokens: 8192,
|
|
137
|
+
},
|
|
138
|
+
[OPUS]: {
|
|
139
|
+
inputTokens: 300,
|
|
140
|
+
outputTokens: 150,
|
|
141
|
+
cacheReadInputTokens: 0,
|
|
142
|
+
cacheCreationInputTokens: 30,
|
|
143
|
+
costUSD: 0.0456,
|
|
144
|
+
webSearchRequests: 0,
|
|
145
|
+
contextWindow: 200000,
|
|
146
|
+
maxOutputTokens: 8192,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The authoritative return the parser must reproduce byte-identically on a
|
|
154
|
+
* clean exit — sourced from the terminal `result` event, NOT the running
|
|
155
|
+
* accumulator.
|
|
156
|
+
*/
|
|
157
|
+
export const expected = {
|
|
158
|
+
result: "Final answer",
|
|
159
|
+
sessionId: SESSION_ID,
|
|
160
|
+
totalCostUsd: 0.0579,
|
|
161
|
+
modelUsage: {
|
|
162
|
+
[SONNET]: {
|
|
163
|
+
inputTokens: 300,
|
|
164
|
+
outputTokens: 130,
|
|
165
|
+
cacheReadInputTokens: 30,
|
|
166
|
+
cacheCreationInputTokens: 5,
|
|
167
|
+
costUSD: 0.0123,
|
|
168
|
+
webSearchRequests: 0,
|
|
169
|
+
contextWindow: 200000,
|
|
170
|
+
maxOutputTokens: 8192,
|
|
171
|
+
},
|
|
172
|
+
[OPUS]: {
|
|
173
|
+
inputTokens: 300,
|
|
174
|
+
outputTokens: 150,
|
|
175
|
+
cacheReadInputTokens: 0,
|
|
176
|
+
cacheCreationInputTokens: 30,
|
|
177
|
+
costUSD: 0.0456,
|
|
178
|
+
webSearchRequests: 0,
|
|
179
|
+
contextWindow: 200000,
|
|
180
|
+
maxOutputTokens: 8192,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The running per-model total the streaming parser accumulates from the
|
|
187
|
+
* `assistant` lines alone (deduped by message.id). costUSD is 0 because cost is
|
|
188
|
+
* not known until the terminal `result` event. This is what the final
|
|
189
|
+
* `onProgress` callback should receive.
|
|
190
|
+
*/
|
|
191
|
+
export const expectedAccumulated = {
|
|
192
|
+
[SONNET]: {
|
|
193
|
+
inputTokens: 300, // 100 (msg_1, counted once) + 200 (msg_2)
|
|
194
|
+
outputTokens: 130, // 50 + 80
|
|
195
|
+
cacheReadInputTokens: 30, // 10 + 20
|
|
196
|
+
cacheCreationInputTokens: 5, // 5 + 0
|
|
197
|
+
costUSD: 0,
|
|
198
|
+
},
|
|
199
|
+
[OPUS]: {
|
|
200
|
+
inputTokens: 300,
|
|
201
|
+
outputTokens: 150,
|
|
202
|
+
cacheReadInputTokens: 0,
|
|
203
|
+
cacheCreationInputTokens: 30,
|
|
204
|
+
costUSD: 0,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Distinct message ids in emission order — onProgress should fire once each. */
|
|
209
|
+
export const expectedProgressCount = 3;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored synthetic `claude -p --output-format stream-json --verbose`
|
|
3
|
+
* transcript exercising `agent/src/runtime/progress-milestones.ts`'s block → phase
|
|
4
|
+
* mapping end-to-end through `_consumeStream`'s `handleLine` closure.
|
|
5
|
+
*
|
|
6
|
+
* Shape of the session:
|
|
7
|
+
* - one `system`/`init` line
|
|
8
|
+
* - assistant turn `msg_1`: a `thinking` block, then a `tool_use` Read block
|
|
9
|
+
* (no `usage` field on this line — a tool_use-only line must still be able
|
|
10
|
+
* to fire a phase update, exercised BEFORE the usage guard)
|
|
11
|
+
* - assistant turn `msg_2`: a second `tool_use` Read block (same phase as
|
|
12
|
+
* msg_1's Read — repeated identical phases must collapse to a single fire)
|
|
13
|
+
* - assistant turn `msg_3`: an `mcp__*`-prefixed tool name → generic `tool`
|
|
14
|
+
* - assistant turn `msg_4`: an unrecognized/unknown tool name → generic `tool`
|
|
15
|
+
* - assistant turn `msg_5`: a plain `text` block → `writing`
|
|
16
|
+
* - one terminal `result`/success line
|
|
17
|
+
*
|
|
18
|
+
* `tool_use` blocks intentionally carry `input` payloads containing
|
|
19
|
+
* fake-secret-shaped values and absolute filesystem paths — the mapper must
|
|
20
|
+
* never read `block.input`, so these values must never surface anywhere in
|
|
21
|
+
* the resulting phase sequence.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const SONNET = "claude-sonnet-4-6";
|
|
25
|
+
const SESSION_ID = "sess-progress-phases";
|
|
26
|
+
|
|
27
|
+
export const lines: string[] = [
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: "system",
|
|
30
|
+
subtype: "init",
|
|
31
|
+
session_id: SESSION_ID,
|
|
32
|
+
model: SONNET,
|
|
33
|
+
tools: ["Read", "Grep", "Bash"],
|
|
34
|
+
}),
|
|
35
|
+
// msg_1 — thinking block, then a tool_use Read block. No `usage` field:
|
|
36
|
+
// exercises firing a phase BEFORE the existing usage guard.
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
type: "assistant",
|
|
39
|
+
message: {
|
|
40
|
+
id: "msg_1",
|
|
41
|
+
role: "assistant",
|
|
42
|
+
model: SONNET,
|
|
43
|
+
content: [
|
|
44
|
+
{ type: "thinking", thinking: "Let me look at the file first." },
|
|
45
|
+
{
|
|
46
|
+
type: "tool_use",
|
|
47
|
+
id: "tu_1",
|
|
48
|
+
name: "Read",
|
|
49
|
+
input: {
|
|
50
|
+
file_path: "/home/alice/.ssh/id_rsa",
|
|
51
|
+
api_key: "sk-fake-12345",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
session_id: SESSION_ID,
|
|
57
|
+
}),
|
|
58
|
+
// tool-result feedback — out of scope for phase mapping
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
type: "user",
|
|
61
|
+
message: {
|
|
62
|
+
role: "user",
|
|
63
|
+
content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }],
|
|
64
|
+
},
|
|
65
|
+
session_id: SESSION_ID,
|
|
66
|
+
}),
|
|
67
|
+
// msg_2 — a SECOND Read tool_use block: same phase ("reading") as msg_1's
|
|
68
|
+
// Read block — must collapse to a single fire (no new phase event).
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
type: "assistant",
|
|
71
|
+
message: {
|
|
72
|
+
id: "msg_2",
|
|
73
|
+
role: "assistant",
|
|
74
|
+
model: SONNET,
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "tool_use",
|
|
78
|
+
id: "tu_2",
|
|
79
|
+
name: "Read",
|
|
80
|
+
input: {
|
|
81
|
+
command: "cat /home/alice/.ssh/id_rsa",
|
|
82
|
+
api_key: "sk-fake-12345",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
usage: {
|
|
87
|
+
input_tokens: 50,
|
|
88
|
+
output_tokens: 10,
|
|
89
|
+
cache_read_input_tokens: 0,
|
|
90
|
+
cache_creation_input_tokens: 0,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
session_id: SESSION_ID,
|
|
94
|
+
}),
|
|
95
|
+
// msg_3 — an mcp__*-prefixed tool name → generic "tool" phase.
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
type: "assistant",
|
|
98
|
+
message: {
|
|
99
|
+
id: "msg_3",
|
|
100
|
+
role: "assistant",
|
|
101
|
+
model: SONNET,
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "tool_use",
|
|
105
|
+
id: "tu_3",
|
|
106
|
+
name: "mcp__linear__create_issue",
|
|
107
|
+
input: { api_key: "sk-fake-12345" },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
usage: {
|
|
111
|
+
input_tokens: 20,
|
|
112
|
+
output_tokens: 5,
|
|
113
|
+
cache_read_input_tokens: 0,
|
|
114
|
+
cache_creation_input_tokens: 0,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
session_id: SESSION_ID,
|
|
118
|
+
}),
|
|
119
|
+
// msg_4 — an unrecognized/unknown tool name → generic "tool" phase.
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
type: "assistant",
|
|
122
|
+
message: {
|
|
123
|
+
id: "msg_4",
|
|
124
|
+
role: "assistant",
|
|
125
|
+
model: SONNET,
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "tool_use",
|
|
129
|
+
id: "tu_4",
|
|
130
|
+
name: "SomeFutureTool",
|
|
131
|
+
input: { secret: "sk-fake-12345" },
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
usage: {
|
|
135
|
+
input_tokens: 20,
|
|
136
|
+
output_tokens: 5,
|
|
137
|
+
cache_read_input_tokens: 0,
|
|
138
|
+
cache_creation_input_tokens: 0,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
session_id: SESSION_ID,
|
|
142
|
+
}),
|
|
143
|
+
// msg_5 — plain text block → "writing".
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
type: "assistant",
|
|
146
|
+
message: {
|
|
147
|
+
id: "msg_5",
|
|
148
|
+
role: "assistant",
|
|
149
|
+
model: SONNET,
|
|
150
|
+
content: [{ type: "text", text: "Here's what I found." }],
|
|
151
|
+
usage: {
|
|
152
|
+
input_tokens: 30,
|
|
153
|
+
output_tokens: 15,
|
|
154
|
+
cache_read_input_tokens: 0,
|
|
155
|
+
cache_creation_input_tokens: 0,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
session_id: SESSION_ID,
|
|
159
|
+
}),
|
|
160
|
+
// terminal result
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
type: "result",
|
|
163
|
+
subtype: "success",
|
|
164
|
+
is_error: false,
|
|
165
|
+
result: "Here's what I found.",
|
|
166
|
+
session_id: SESSION_ID,
|
|
167
|
+
usage: {
|
|
168
|
+
input_tokens: 120,
|
|
169
|
+
output_tokens: 35,
|
|
170
|
+
cache_read_input_tokens: 0,
|
|
171
|
+
cache_creation_input_tokens: 0,
|
|
172
|
+
},
|
|
173
|
+
total_cost_usd: 0.0042,
|
|
174
|
+
modelUsage: {
|
|
175
|
+
[SONNET]: {
|
|
176
|
+
inputTokens: 120,
|
|
177
|
+
outputTokens: 35,
|
|
178
|
+
cacheReadInputTokens: 0,
|
|
179
|
+
cacheCreationInputTokens: 0,
|
|
180
|
+
costUSD: 0.0042,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The ordered sequence of DISTINCT phase fires `onProgress`/`perCallOnProgress`
|
|
188
|
+
* should emit across this fixture (collapsed — a phase identical to the
|
|
189
|
+
* immediately preceding fired phase does not fire again). `msg_1`'s thinking
|
|
190
|
+
* block fires "thinking" first, then its Read tool_use fires "reading".
|
|
191
|
+
* `msg_2`'s Read tool_use maps to the same "reading" phase as msg_1's —
|
|
192
|
+
* collapsed, no second fire. `msg_3` (mcp__*) maps to the generic "tool"
|
|
193
|
+
* phase — a new fire (differs from "reading"). `msg_4` (unknown tool) ALSO
|
|
194
|
+
* maps to "tool" — identical to the immediately preceding fired phase, so it
|
|
195
|
+
* collapses too (no second "tool" fire). `msg_5`'s text block fires "writing".
|
|
196
|
+
*/
|
|
197
|
+
export const expectedPhaseSequence = ["thinking", "reading", "tool", "writing"];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fake-secret-shaped substrings embedded in this fixture's `tool_use.input`
|
|
201
|
+
* payloads — must never appear anywhere in the phase output.
|
|
202
|
+
*/
|
|
203
|
+
export const forbiddenSubstrings = ["/home/alice/.ssh/id_rsa", "sk-fake-12345"];
|