@syengup/friday-channel-next 0.0.34 → 0.0.37
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/dist/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
- package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
- package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
- package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
- package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
- package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
- package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
- package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
- package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
- package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
- package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
- package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
- package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
- package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
- package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
- package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
- package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
- package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
- package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
- package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
- package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
- package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
- package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
- package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
- package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
- package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
- package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
- package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
- package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
- package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
- package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
- package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
- package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
- package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
- package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
- package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
- package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
- package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
- package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +176 -0
- package/dist/src/agent/abort-run.d.ts +1 -0
- package/dist/src/agent/abort-run.js +11 -0
- package/dist/src/agent/active-runs.d.ts +9 -0
- package/dist/src/agent/active-runs.js +20 -0
- package/dist/src/agent/dispatch-bridge.d.ts +5 -0
- package/dist/src/agent/dispatch-bridge.js +12 -0
- package/dist/src/agent/media-bridge.d.ts +4 -0
- package/dist/src/agent/media-bridge.js +21 -0
- package/dist/src/agent/subagent-registry.d.ts +68 -0
- package/dist/src/agent/subagent-registry.js +142 -0
- package/dist/src/agent-forward-runtime.d.ts +17 -0
- package/dist/src/agent-forward-runtime.js +16 -0
- package/dist/src/agent-run-context-bridge.d.ts +13 -0
- package/dist/src/agent-run-context-bridge.js +23 -0
- package/dist/src/channel-actions.d.ts +13 -0
- package/dist/src/channel-actions.js +101 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +248 -0
- package/dist/src/collect-message-media-paths.d.ts +11 -0
- package/dist/src/collect-message-media-paths.js +143 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +39 -0
- package/dist/src/friday-inbound-stats.d.ts +2 -0
- package/dist/src/friday-inbound-stats.js +8 -0
- package/dist/src/friday-session.d.ts +40 -0
- package/dist/src/friday-session.js +395 -0
- package/dist/src/host-config.d.ts +1 -0
- package/dist/src/host-config.js +15 -0
- package/dist/src/http/handlers/cancel.d.ts +2 -0
- package/dist/src/http/handlers/cancel.js +33 -0
- package/dist/src/http/handlers/device-approve.d.ts +2 -0
- package/dist/src/http/handlers/device-approve.js +125 -0
- package/dist/src/http/handlers/device-token.d.ts +2 -0
- package/dist/src/http/handlers/device-token.js +43 -0
- package/dist/src/http/handlers/files-download.d.ts +10 -0
- package/dist/src/http/handlers/files-download.js +210 -0
- package/dist/src/http/handlers/files-upload.d.ts +8 -0
- package/dist/src/http/handlers/files-upload.js +136 -0
- package/dist/src/http/handlers/files.d.ts +75 -0
- package/dist/src/http/handlers/files.js +305 -0
- package/dist/src/http/handlers/import.d.ts +7 -0
- package/dist/src/http/handlers/import.js +69 -0
- package/dist/src/http/handlers/info.d.ts +2 -0
- package/dist/src/http/handlers/info.js +13 -0
- package/dist/src/http/handlers/messages-list.d.ts +7 -0
- package/dist/src/http/handlers/messages-list.js +44 -0
- package/dist/src/http/handlers/messages.d.ts +34 -0
- package/dist/src/http/handlers/messages.js +476 -0
- package/dist/src/http/handlers/models-list.d.ts +10 -0
- package/dist/src/http/handlers/models-list.js +113 -0
- package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
- package/dist/src/http/handlers/nodes-approve.js +146 -0
- package/dist/src/http/handlers/pair.d.ts +2 -0
- package/dist/src/http/handlers/pair.js +39 -0
- package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
- package/dist/src/http/handlers/sessions-delete.js +49 -0
- package/dist/src/http/handlers/sessions-list.d.ts +8 -0
- package/dist/src/http/handlers/sessions-list.js +24 -0
- package/dist/src/http/handlers/sessions-messages-get.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages-get.js +55 -0
- package/dist/src/http/handlers/sessions-messages-post.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages-post.js +92 -0
- package/dist/src/http/handlers/sessions-messages.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages.js +135 -0
- package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
- package/dist/src/http/handlers/sessions-settings.js +71 -0
- package/dist/src/http/handlers/sse.d.ts +2 -0
- package/dist/src/http/handlers/sse.js +70 -0
- package/dist/src/http/handlers/status.d.ts +2 -0
- package/dist/src/http/handlers/status.js +29 -0
- package/dist/src/http/handlers/sync.d.ts +7 -0
- package/dist/src/http/handlers/sync.js +56 -0
- package/dist/src/http/middleware/auth.d.ts +13 -0
- package/dist/src/http/middleware/auth.js +29 -0
- package/dist/src/http/middleware/body.d.ts +2 -0
- package/dist/src/http/middleware/body.js +24 -0
- package/dist/src/http/middleware/cors.d.ts +2 -0
- package/dist/src/http/middleware/cors.js +11 -0
- package/dist/src/http/server.d.ts +19 -0
- package/dist/src/http/server.js +87 -0
- package/dist/src/logging.d.ts +7 -0
- package/dist/src/logging.js +28 -0
- package/dist/src/push/apns.d.ts +15 -0
- package/dist/src/push/apns.js +56 -0
- package/dist/src/push/device-tokens.d.ts +3 -0
- package/dist/src/push/device-tokens.js +39 -0
- package/dist/src/run-metadata.d.ts +25 -0
- package/dist/src/run-metadata.js +139 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/session/session-manager.d.ts +22 -0
- package/dist/src/session/session-manager.js +190 -0
- package/dist/src/session-usage-snapshot.d.ts +23 -0
- package/dist/src/session-usage-snapshot.js +65 -0
- package/dist/src/sse/emitter.d.ts +59 -0
- package/dist/src/sse/emitter.js +219 -0
- package/dist/src/sse/offline-queue.d.ts +26 -0
- package/dist/src/sse/offline-queue.js +134 -0
- package/dist/src/sync/account-identity.d.ts +14 -0
- package/dist/src/sync/account-identity.js +101 -0
- package/dist/src/sync/archive.d.ts +9 -0
- package/dist/src/sync/archive.js +25 -0
- package/dist/src/sync/database.d.ts +66 -0
- package/dist/src/sync/database.js +364 -0
- package/dist/src/sync/init.d.ts +3 -0
- package/dist/src/sync/init.js +14 -0
- package/dist/src/sync/installation-id.d.ts +1 -0
- package/dist/src/sync/installation-id.js +41 -0
- package/dist/src/sync/message-accumulator.d.ts +29 -0
- package/dist/src/sync/message-accumulator.js +188 -0
- package/dist/src/sync/message-store.d.ts +68 -0
- package/dist/src/sync/message-store.js +262 -0
- package/dist/src/sync/push-store.d.ts +5 -0
- package/dist/src/sync/push-store.js +54 -0
- package/dist/src/sync/session-key.d.ts +12 -0
- package/dist/src/sync/session-key.js +47 -0
- package/dist/src/sync/sync-state.d.ts +5 -0
- package/dist/src/sync/sync-state.js +54 -0
- package/dist/src/sync/transcript-archive.d.ts +13 -0
- package/dist/src/sync/transcript-archive.js +37 -0
- package/dist/src/sync/transcript-store.d.ts +35 -0
- package/dist/src/sync/transcript-store.js +221 -0
- package/dist/src/sync/translate.d.ts +42 -0
- package/dist/src/sync/translate.js +171 -0
- package/dist/src/vendor/runtime-store.d.ts +26 -0
- package/dist/src/vendor/runtime-store.js +60 -0
- package/install.js +2 -2
- package/package.json +11 -10
- package/src/agent/subagent-registry.ts +195 -0
- package/src/channel.ts +6 -4
- package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
- package/src/e2e/subagent.e2e.test.ts +502 -0
- package/src/friday-session.ts +140 -1
- package/src/http/handlers/device-approve.test.ts +0 -1
- package/src/http/handlers/device-approve.ts +0 -2
- package/src/http/handlers/files-download.ts +4 -1
- package/src/http/handlers/files.ts +7 -4
- package/src/http/handlers/messages.ts +54 -4
- package/src/http/handlers/models-list.ts +24 -2
- package/src/http/handlers/nodes-approve.test.ts +288 -0
- package/src/http/handlers/nodes-approve.ts +189 -0
- package/src/http/server.ts +5 -0
- package/src/openclaw.d.ts +5 -0
- package/src/sse/emitter.ts +1 -1
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export type SubagentStatus = "spawning" | "running" | "ended";
|
|
2
|
+
|
|
3
|
+
export type SubagentOutcome = "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
|
|
4
|
+
|
|
5
|
+
export type SubagentEntry = {
|
|
6
|
+
childSessionKey: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
parentRunId?: string;
|
|
9
|
+
deviceId: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
depth: number;
|
|
12
|
+
status: SubagentStatus;
|
|
13
|
+
outcome?: SubagentOutcome;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SubagentMeta = {
|
|
18
|
+
label?: string;
|
|
19
|
+
parentRunId?: string;
|
|
20
|
+
depth: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SpawnIntent = {
|
|
24
|
+
label?: string;
|
|
25
|
+
parentRunId: string;
|
|
26
|
+
deviceId: string;
|
|
27
|
+
depth: number;
|
|
28
|
+
requesterSessionKey?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const byChildSessionKey = new Map<string, SubagentEntry>();
|
|
32
|
+
const byRunId = new Map<string, SubagentEntry>();
|
|
33
|
+
const byToolCallId = new Map<string, SpawnIntent>();
|
|
34
|
+
const sessionKeyToRunId = new Map<string, string>();
|
|
35
|
+
|
|
36
|
+
/** Called by forwardAgentEventRaw on lifecycle start so we can resolve parentRunId. */
|
|
37
|
+
export function registerSessionKeyForRun(sessionKey: string, runId: string): void {
|
|
38
|
+
if (!sessionKey || !runId) return;
|
|
39
|
+
sessionKeyToRunId.set(sessionKey, runId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveRunIdForSessionKey(sessionKey: string): string | undefined {
|
|
43
|
+
return sessionKeyToRunId.get(sessionKey);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse OpenClaw announce compound runId:
|
|
48
|
+
* announce:v<version>:<sessionKey>:<bareRunId>
|
|
49
|
+
* Example:
|
|
50
|
+
* announce:v1:agent:main:subagent:uuid:runId
|
|
51
|
+
* → { childSessionKey: "agent:main:subagent:uuid", bareRunId: "runId" }
|
|
52
|
+
*/
|
|
53
|
+
const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
|
|
54
|
+
|
|
55
|
+
function parseAnnounceRunId(runId: string): { childSessionKey: string; bareRunId: string } | null {
|
|
56
|
+
const m = runId.match(ANNOUNCE_RUN_ID_RE);
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
return { childSessionKey: m[1] ?? "", bareRunId: m[2] ?? "" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Look up subagent entry by any runId form (announce compound or bare). */
|
|
62
|
+
export function lookupByRunId(runId: string): SubagentEntry | undefined {
|
|
63
|
+
const direct = byRunId.get(runId);
|
|
64
|
+
if (direct) return direct;
|
|
65
|
+
const parsed = parseAnnounceRunId(runId);
|
|
66
|
+
if (parsed) {
|
|
67
|
+
const byChild = byChildSessionKey.get(parsed.childSessionKey);
|
|
68
|
+
if (byChild) {
|
|
69
|
+
// Also register the compound runId for future fast lookup
|
|
70
|
+
byRunId.set(runId, byChild);
|
|
71
|
+
return byChild;
|
|
72
|
+
}
|
|
73
|
+
return byRunId.get(parsed.bareRunId);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Look up subagent entry by childSessionKey. */
|
|
79
|
+
export function lookupByChildSessionKey(key: string): SubagentEntry | undefined {
|
|
80
|
+
return byChildSessionKey.get(key);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Called on sessions_spawn item.start (before tool execution).
|
|
85
|
+
* Stores the spawn intent so spawning SSE can be emitted immediately.
|
|
86
|
+
*/
|
|
87
|
+
export function registerSpawnIntent(params: {
|
|
88
|
+
toolCallId: string;
|
|
89
|
+
label?: string;
|
|
90
|
+
deviceId: string;
|
|
91
|
+
parentRunId: string;
|
|
92
|
+
requesterSessionKey?: string;
|
|
93
|
+
}): SpawnIntent {
|
|
94
|
+
let depth = 1;
|
|
95
|
+
if (params.requesterSessionKey) {
|
|
96
|
+
const parent = byChildSessionKey.get(params.requesterSessionKey);
|
|
97
|
+
if (parent) depth = parent.depth + 1;
|
|
98
|
+
}
|
|
99
|
+
const intent: SpawnIntent = {
|
|
100
|
+
label: params.label,
|
|
101
|
+
parentRunId: params.parentRunId,
|
|
102
|
+
deviceId: params.deviceId.toUpperCase(),
|
|
103
|
+
depth,
|
|
104
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
105
|
+
};
|
|
106
|
+
byToolCallId.set(params.toolCallId, intent);
|
|
107
|
+
return intent;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Consume a previously registered spawn intent. Returns undefined if none. */
|
|
111
|
+
export function consumeSpawnIntent(toolCallId: string): SpawnIntent | undefined {
|
|
112
|
+
const intent = byToolCallId.get(toolCallId);
|
|
113
|
+
byToolCallId.delete(toolCallId);
|
|
114
|
+
return intent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Called when forwardAgentEventRaw sees a sessions_spawn tool result.
|
|
119
|
+
* Extracts childSessionKey, runId, taskName and registers the subagent.
|
|
120
|
+
*/
|
|
121
|
+
export function ensureSubagentFromSpawnTool(params: {
|
|
122
|
+
childSessionKey: string;
|
|
123
|
+
bareRunId?: string;
|
|
124
|
+
label?: string;
|
|
125
|
+
deviceId: string;
|
|
126
|
+
parentRunId: string;
|
|
127
|
+
requesterSessionKey?: string;
|
|
128
|
+
depth?: number;
|
|
129
|
+
}): SubagentEntry {
|
|
130
|
+
const existing = byChildSessionKey.get(params.childSessionKey);
|
|
131
|
+
if (existing) {
|
|
132
|
+
if (params.bareRunId && !existing.runId) {
|
|
133
|
+
existing.runId = params.bareRunId;
|
|
134
|
+
byRunId.set(params.bareRunId, existing);
|
|
135
|
+
sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
|
|
136
|
+
}
|
|
137
|
+
existing.status = "running";
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let depth = params.depth ?? 1;
|
|
142
|
+
if (depth <= 1 && params.requesterSessionKey) {
|
|
143
|
+
const parent = byChildSessionKey.get(params.requesterSessionKey);
|
|
144
|
+
if (parent) depth = parent.depth + 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const entry: SubagentEntry = {
|
|
148
|
+
childSessionKey: params.childSessionKey,
|
|
149
|
+
runId: params.bareRunId,
|
|
150
|
+
parentRunId: params.parentRunId,
|
|
151
|
+
deviceId: params.deviceId.toUpperCase(),
|
|
152
|
+
label: params.label || undefined,
|
|
153
|
+
depth,
|
|
154
|
+
status: "running",
|
|
155
|
+
};
|
|
156
|
+
byChildSessionKey.set(params.childSessionKey, entry);
|
|
157
|
+
if (params.bareRunId) {
|
|
158
|
+
byRunId.set(params.bareRunId, entry);
|
|
159
|
+
sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
|
|
160
|
+
}
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Mark subagent as ended. Resolves by bare/compound runId or childSessionKey. */
|
|
165
|
+
export function registerEnded(params: {
|
|
166
|
+
runId?: string;
|
|
167
|
+
childSessionKey?: string;
|
|
168
|
+
outcome?: SubagentOutcome;
|
|
169
|
+
error?: string;
|
|
170
|
+
}): SubagentEntry | undefined {
|
|
171
|
+
let entry: SubagentEntry | undefined;
|
|
172
|
+
if (params.runId) entry = lookupByRunId(params.runId);
|
|
173
|
+
if (!entry && params.childSessionKey) entry = byChildSessionKey.get(params.childSessionKey);
|
|
174
|
+
if (!entry) return undefined;
|
|
175
|
+
entry.status = "ended";
|
|
176
|
+
if (params.outcome) entry.outcome = params.outcome;
|
|
177
|
+
if (params.error) entry.error = params.error;
|
|
178
|
+
return entry;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Extract subagent metadata for SSE annotation. */
|
|
182
|
+
export function subagentMeta(entry: SubagentEntry): SubagentMeta {
|
|
183
|
+
return {
|
|
184
|
+
label: entry.label,
|
|
185
|
+
parentRunId: entry.parentRunId,
|
|
186
|
+
depth: entry.depth,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function resetForTest(): void {
|
|
191
|
+
byChildSessionKey.clear();
|
|
192
|
+
byRunId.clear();
|
|
193
|
+
byToolCallId.clear();
|
|
194
|
+
sessionKeyToRunId.clear();
|
|
195
|
+
}
|
package/src/channel.ts
CHANGED
|
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { createFridayNextLogger } from "./logging.js";
|
|
11
12
|
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
|
12
13
|
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
|
|
13
14
|
import { sseEmitter } from "./sse/emitter.js";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./friday-session.js";
|
|
20
21
|
import { getLastFridayInboundAt } from "./friday-inbound-stats.js";
|
|
21
22
|
|
|
23
|
+
const logger = createFridayNextLogger("channel");
|
|
22
24
|
const CHANNEL_ID = "friday-next" as const;
|
|
23
25
|
|
|
24
26
|
function pickFirstString(source: Record<string, unknown>, keys: string[]): string | undefined {
|
|
@@ -156,8 +158,8 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
|
|
|
156
158
|
|
|
157
159
|
const conn = sseEmitter.getConnection(deviceId);
|
|
158
160
|
const ts = new Date().toISOString();
|
|
159
|
-
|
|
160
|
-
`[
|
|
161
|
+
logger.info(
|
|
162
|
+
`[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
|
|
161
163
|
);
|
|
162
164
|
|
|
163
165
|
if (conn) {
|
|
@@ -243,8 +245,8 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
|
|
|
243
245
|
|
|
244
246
|
const conn = sseEmitter.getConnection(deviceId);
|
|
245
247
|
const ts = new Date().toISOString();
|
|
246
|
-
|
|
247
|
-
`[
|
|
248
|
+
logger.info(
|
|
249
|
+
`[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
|
|
248
250
|
);
|
|
249
251
|
|
|
250
252
|
if (conn) {
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent SSE smoke test — simulates the Friday iOS app connecting and
|
|
3
|
+
* receiving a complete subagent lifecycle via sessions_spawn tool events.
|
|
4
|
+
*
|
|
5
|
+
* Run with:
|
|
6
|
+
* pnpm vitest run --config vitest.e2e.config.ts src/e2e/subagent-smoke.e2e.test.ts --reporter verbose
|
|
7
|
+
*/
|
|
8
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
10
|
+
import { createTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureSubagentFromSpawnTool,
|
|
13
|
+
resetForTest as resetSubagentRegistry,
|
|
14
|
+
} from "../agent/subagent-registry.js";
|
|
15
|
+
import {
|
|
16
|
+
forwardAgentEventRaw,
|
|
17
|
+
registerFridaySessionDeviceMapping,
|
|
18
|
+
} from "../friday-session.js";
|
|
19
|
+
import { sseEmitter } from "../sse/emitter.js";
|
|
20
|
+
|
|
21
|
+
const deviceId = "IOS-DEVICE-AAAA-BBBB-CCCC-DDDD";
|
|
22
|
+
const mainSessionKey = "agent:main:main";
|
|
23
|
+
const mainRunId = "main-run-001";
|
|
24
|
+
|
|
25
|
+
function spawnResult(childSessionKey: string, runId: string, taskName: string) {
|
|
26
|
+
return {
|
|
27
|
+
childSessionKey,
|
|
28
|
+
runId,
|
|
29
|
+
taskName,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function describeFrame(frame: { event?: string; data?: Record<string, unknown> }) {
|
|
34
|
+
const event = frame.event ?? "?";
|
|
35
|
+
const data = frame.data ?? {};
|
|
36
|
+
switch (event) {
|
|
37
|
+
case "connected":
|
|
38
|
+
return `connected deviceId=${data.deviceId} lastSeq=${data.lastSeq}`;
|
|
39
|
+
case "subagent": {
|
|
40
|
+
const extra = data.phase === "ended" ? ` outcome=${data.outcome} error=${data.error ?? "-"}` : "";
|
|
41
|
+
return `subagent phase=${data.phase} runId=${data.runId ?? "(pending)"} label=${data.label ?? "-"} parentRunId=${data.parentRunId ?? "-"} depth=${data.depth}${extra}`;
|
|
42
|
+
}
|
|
43
|
+
case "agent": {
|
|
44
|
+
const sub = data.subagent as Record<string, unknown> | undefined;
|
|
45
|
+
const subTag = sub ? ` SUB="agent:${sub.label ?? "?"}" depth=${sub.depth} parent=${sub.parentRunId ?? "-"}` : " (main)";
|
|
46
|
+
const inner = (data.data ?? {}) as Record<string, unknown>;
|
|
47
|
+
let detail = "";
|
|
48
|
+
if (data.stream === "thinking") detail = ` thinking: "${String(inner.text ?? "").slice(0, 50)}"`;
|
|
49
|
+
else if (data.stream === "tool") detail = ` tool=${inner.name ?? "?"} phase=${inner.phase}`;
|
|
50
|
+
else if (data.stream === "assistant") detail = ` text="${String(inner.text ?? inner.phase ?? "")}"`;
|
|
51
|
+
else if (data.stream === "lifecycle") detail = ` phase=${inner.phase}`;
|
|
52
|
+
return `agent stream=${data.stream} runId=${String(data.runId).slice(0, 30)}…${subTag}${detail}`;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return `${event}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("subagent smoke", () => {
|
|
60
|
+
let historyDir = "";
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
historyDir = createTempHistoryDir();
|
|
64
|
+
setMockRuntime({ historyDir, authToken: "smoke-token" });
|
|
65
|
+
resetSubagentRegistry();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("full nested subagent lifecycle via sessions_spawn tool events", async () => {
|
|
69
|
+
const app = createAppSimulator({ token: "smoke-token", deviceId });
|
|
70
|
+
await app.connectSSE();
|
|
71
|
+
registerFridaySessionDeviceMapping(mainSessionKey, deviceId);
|
|
72
|
+
|
|
73
|
+
// ── Main run lifecycle start ──────────────────────────
|
|
74
|
+
forwardAgentEventRaw({
|
|
75
|
+
runId: mainRunId, seq: 1, stream: "lifecycle",
|
|
76
|
+
sessionKey: mainSessionKey, data: { phase: "start" },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── Subagent A: tool.start (spawning) ─────────────────
|
|
80
|
+
const childKeyA = "agent:main:subagent:code-reviewer";
|
|
81
|
+
const bareA = "bare-run-reviewer";
|
|
82
|
+
const compoundA = `announce:v1:${childKeyA}:${bareA}`;
|
|
83
|
+
|
|
84
|
+
forwardAgentEventRaw({
|
|
85
|
+
runId: mainRunId, seq: 2, stream: "tool",
|
|
86
|
+
sessionKey: mainSessionKey,
|
|
87
|
+
data: {
|
|
88
|
+
phase: "start", name: "sessions_spawn",
|
|
89
|
+
toolCallId: "call_a",
|
|
90
|
+
args: { taskName: "code-reviewer" },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── Subagent A: tool.result (spawned) ─────────────────
|
|
95
|
+
forwardAgentEventRaw({
|
|
96
|
+
runId: mainRunId, seq: 3, stream: "tool",
|
|
97
|
+
sessionKey: mainSessionKey,
|
|
98
|
+
data: {
|
|
99
|
+
phase: "result", name: "sessions_spawn", toolCallId: "call_1",
|
|
100
|
+
meta: "code-reviewer",
|
|
101
|
+
result: { details: spawnResult(childKeyA, bareA, "code-reviewer") },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
sseEmitter.trackDeviceForRun(deviceId, compoundA);
|
|
105
|
+
|
|
106
|
+
// ── Subagent A: agent events (subagent's own sessionKey) ─
|
|
107
|
+
forwardAgentEventRaw({
|
|
108
|
+
runId: compoundA, seq: 1, stream: "lifecycle",
|
|
109
|
+
data: { phase: "start" }, sessionKey: childKeyA,
|
|
110
|
+
});
|
|
111
|
+
forwardAgentEventRaw({
|
|
112
|
+
runId: compoundA, seq: 2, stream: "thinking",
|
|
113
|
+
data: { text: "Let me review the code for issues…", delta: "Let me review the code for issues…", reasoningPrefixChars: 0 },
|
|
114
|
+
sessionKey: childKeyA,
|
|
115
|
+
});
|
|
116
|
+
forwardAgentEventRaw({
|
|
117
|
+
runId: compoundA, seq: 3, stream: "tool",
|
|
118
|
+
data: { phase: "start", name: "read", args: { path: "src/app.ts" } },
|
|
119
|
+
sessionKey: childKeyA,
|
|
120
|
+
});
|
|
121
|
+
forwardAgentEventRaw({
|
|
122
|
+
runId: compoundA, seq: 4, stream: "tool",
|
|
123
|
+
data: { phase: "result", name: "read", result: "file contents…" },
|
|
124
|
+
sessionKey: childKeyA,
|
|
125
|
+
});
|
|
126
|
+
forwardAgentEventRaw({
|
|
127
|
+
runId: compoundA, seq: 5, stream: "assistant",
|
|
128
|
+
data: { phase: "delta", text: "Found 3 issues in the code." },
|
|
129
|
+
sessionKey: childKeyA,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── Nested: Subagent B spawning + spawned ────────────
|
|
133
|
+
const childKeyB = "agent:main:subagent:lint";
|
|
134
|
+
const bareB = "bare-run-lint";
|
|
135
|
+
const compoundB = `announce:v1:${childKeyB}:${bareB}`;
|
|
136
|
+
|
|
137
|
+
forwardAgentEventRaw({
|
|
138
|
+
runId: compoundA, seq: 7, stream: "tool",
|
|
139
|
+
sessionKey: mainSessionKey,
|
|
140
|
+
data: {
|
|
141
|
+
phase: "start", name: "sessions_spawn",
|
|
142
|
+
toolCallId: "call_b",
|
|
143
|
+
args: { taskName: "lint" },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
forwardAgentEventRaw({
|
|
148
|
+
runId: compoundA, seq: 8, stream: "tool",
|
|
149
|
+
sessionKey: mainSessionKey,
|
|
150
|
+
data: {
|
|
151
|
+
phase: "result", name: "sessions_spawn", toolCallId: "call_2",
|
|
152
|
+
meta: "lint",
|
|
153
|
+
result: { details: spawnResult(childKeyB, bareB, "lint") },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
sseEmitter.trackDeviceForRun(deviceId, compoundB);
|
|
157
|
+
|
|
158
|
+
// ── Subagent B: agent event (subagent's own sessionKey) ─
|
|
159
|
+
forwardAgentEventRaw({
|
|
160
|
+
runId: compoundB, seq: 1, stream: "assistant",
|
|
161
|
+
data: { phase: "delta", text: "No lint errors found." },
|
|
162
|
+
sessionKey: childKeyB,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Subagent B ended (subagent's own sessionKey) ──────
|
|
166
|
+
forwardAgentEventRaw({
|
|
167
|
+
runId: compoundB, seq: 2, stream: "lifecycle",
|
|
168
|
+
data: { phase: "end" },
|
|
169
|
+
sessionKey: childKeyB,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── Subagent A ended (subagent's own sessionKey) ──────
|
|
173
|
+
forwardAgentEventRaw({
|
|
174
|
+
runId: compoundA, seq: 7, stream: "lifecycle",
|
|
175
|
+
data: { phase: "end" },
|
|
176
|
+
sessionKey: childKeyA,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Wait for SSE flush
|
|
180
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
181
|
+
const frames = app.getSseFrames();
|
|
182
|
+
|
|
183
|
+
// ── Verbose output ──────────────────────────────────
|
|
184
|
+
console.log("\n══════════════════════════════════════════════════════");
|
|
185
|
+
console.log(" SSE Frames (as received by the iOS app)");
|
|
186
|
+
console.log("══════════════════════════════════════════════════════\n");
|
|
187
|
+
for (const frame of frames) {
|
|
188
|
+
console.log(`[${String(frame.id ?? "-").padStart(3)}] ${describeFrame(frame)}`);
|
|
189
|
+
}
|
|
190
|
+
console.log("\n══════════════════════════════════════════════════════");
|
|
191
|
+
const subagentCount = frames.filter((f) => f.event === "subagent").length;
|
|
192
|
+
const annotatedCount = frames.filter(
|
|
193
|
+
(f) => f.event === "agent" && f.data?.subagent != null,
|
|
194
|
+
).length;
|
|
195
|
+
console.log(` Totals: ${frames.length} frames`);
|
|
196
|
+
console.log(` subagent lifecycle events: ${subagentCount}`);
|
|
197
|
+
console.log(` agent events with subagent annotation: ${annotatedCount}`);
|
|
198
|
+
console.log("══════════════════════════════════════════════════════\n");
|
|
199
|
+
|
|
200
|
+
// ── Assertions ──────────────────────────────────────
|
|
201
|
+
expect(subagentCount).toBeGreaterThanOrEqual(6); // 2 spawning + 2 spawned + 2 ended
|
|
202
|
+
expect(annotatedCount).toBeGreaterThanOrEqual(5);
|
|
203
|
+
|
|
204
|
+
// Verify subagent phases
|
|
205
|
+
const subagentFrames = frames.filter((f) => f.event === "subagent");
|
|
206
|
+
const phases = subagentFrames.map((f) => f.data?.phase);
|
|
207
|
+
expect(phases.filter((p) => p === "spawning")).toHaveLength(2);
|
|
208
|
+
expect(phases.filter((p) => p === "spawned")).toHaveLength(2);
|
|
209
|
+
expect(phases.filter((p) => p === "ended")).toHaveLength(2);
|
|
210
|
+
|
|
211
|
+
// Verify annotation only on subagent events
|
|
212
|
+
for (const f of frames) {
|
|
213
|
+
if (f.event !== "agent") continue;
|
|
214
|
+
const hasSub = f.data?.subagent != null;
|
|
215
|
+
const runId = f.data?.runId as string | undefined;
|
|
216
|
+
if (runId === mainRunId) {
|
|
217
|
+
expect(hasSub, `main agent annotated: ${describeFrame(f)}`).toBe(false);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
app.disconnectSSE();
|
|
222
|
+
});
|
|
223
|
+
});
|