@syengup/friday-channel-next 0.1.39 → 0.1.40
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/src/agent/subagent-registry.d.ts +4 -0
- package/dist/src/agent/subagent-registry.js +1 -1
- package/dist/src/friday-session.js +35 -1
- package/dist/src/http/handlers/agents-list.js +5 -1
- package/package.json +1 -1
- package/src/agent/subagent-registry.ts +3 -1
- package/src/e2e/subagent.e2e.test.ts +6 -0
- package/src/friday-session.forward-agent.test.ts +127 -0
- package/src/friday-session.ts +48 -1
- package/src/http/handlers/agents-list.test.ts +28 -0
- package/src/http/handlers/agents-list.ts +5 -1
|
@@ -25,6 +25,10 @@ type SpawnIntent = {
|
|
|
25
25
|
};
|
|
26
26
|
/** Called by forwardAgentEventRaw on lifecycle start so we can resolve parentRunId. */
|
|
27
27
|
export declare function registerSessionKeyForRun(sessionKey: string, runId: string): void;
|
|
28
|
+
export declare function parseAnnounceRunId(runId: string): {
|
|
29
|
+
childSessionKey: string;
|
|
30
|
+
bareRunId: string;
|
|
31
|
+
} | null;
|
|
28
32
|
/** Look up subagent entry by any runId form (announce compound or bare). */
|
|
29
33
|
export declare function lookupByRunId(runId: string): SubagentEntry | undefined;
|
|
30
34
|
/** Look up subagent entry by childSessionKey. */
|
|
@@ -16,7 +16,7 @@ export function registerSessionKeyForRun(sessionKey, runId) {
|
|
|
16
16
|
* → { childSessionKey: "agent:main:subagent:uuid", bareRunId: "runId" }
|
|
17
17
|
*/
|
|
18
18
|
const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
|
|
19
|
-
function parseAnnounceRunId(runId) {
|
|
19
|
+
export function parseAnnounceRunId(runId) {
|
|
20
20
|
const m = runId.match(ANNOUNCE_RUN_ID_RE);
|
|
21
21
|
if (!m)
|
|
22
22
|
return null;
|
|
@@ -6,7 +6,7 @@ import { observeAgentEventForActiveRuns } from "./agent/active-runs.js";
|
|
|
6
6
|
import { getRunMetadata, ingestAgentEventMetadata } from "./run-metadata.js";
|
|
7
7
|
import { consumeRunUsage } from "./agent/run-usage-accumulator.js";
|
|
8
8
|
import { readSessionUsageSnapshotFromStore } from "./session-usage-store.js";
|
|
9
|
-
import { lookupByRunId, registerSessionKeyForRun, registerSpawnIntent, consumeSpawnIntent, ensureSubagentFromSpawnTool, registerEnded as registerSubagentEnded, } from "./agent/subagent-registry.js";
|
|
9
|
+
import { lookupByRunId, lookupByChildSessionKey, parseAnnounceRunId, registerSessionKeyForRun, registerSpawnIntent, consumeSpawnIntent, ensureSubagentFromSpawnTool, registerEnded as registerSubagentEnded, } from "./agent/subagent-registry.js";
|
|
10
10
|
/** Last `data.text` per run for `stream: "thinking"` — OpenClaw core may send cumulative `delta`; we rewrite true increments for the app. */
|
|
11
11
|
const lastThinkingTextByRun = new Map();
|
|
12
12
|
function commonPrefixLength(a, b) {
|
|
@@ -323,6 +323,10 @@ export function forwardAgentEventRaw(evt) {
|
|
|
323
323
|
phase: "spawning",
|
|
324
324
|
childSessionKey: null,
|
|
325
325
|
runId: null,
|
|
326
|
+
// A2: the spawn tool-call id is the only stable correlation key before the
|
|
327
|
+
// gateway assigns childSessionKey/runId — the app mints the placeholder window
|
|
328
|
+
// under it, then rekeys to childSessionKey on spawned.
|
|
329
|
+
toolCallId,
|
|
326
330
|
label: intent.label ?? null,
|
|
327
331
|
parentRunId: intent.parentRunId,
|
|
328
332
|
depth: intent.depth,
|
|
@@ -357,6 +361,9 @@ export function forwardAgentEventRaw(evt) {
|
|
|
357
361
|
phase: "spawned",
|
|
358
362
|
runId: compoundRunId,
|
|
359
363
|
childSessionKey: entry.childSessionKey,
|
|
364
|
+
// A2: echo the spawn toolCallId so the app deterministically links this
|
|
365
|
+
// spawned event to the placeholder window it minted at spawning time.
|
|
366
|
+
toolCallId: toolCallId || null,
|
|
360
367
|
label: entry.label ?? null,
|
|
361
368
|
parentRunId: entry.parentRunId ?? null,
|
|
362
369
|
depth: entry.depth,
|
|
@@ -365,6 +372,28 @@ export function forwardAgentEventRaw(evt) {
|
|
|
365
372
|
}, entry.deviceId);
|
|
366
373
|
}
|
|
367
374
|
}
|
|
375
|
+
// Phase 3 (A3): announce-summary delivery to the parent. OpenClaw emits the parent's
|
|
376
|
+
// `lifecycle.start` under the announce compound runId once a subagent's result is being
|
|
377
|
+
// folded back in. We parse the authoritative childSessionKey here (the registry already
|
|
378
|
+
// knows how) and broadcast an explicit `dismissed` subagent event, so the app removes the
|
|
379
|
+
// settled window by childSessionKey instead of re-parsing the announce runId itself.
|
|
380
|
+
if (evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
381
|
+
const announced = parseAnnounceRunId(evt.runId);
|
|
382
|
+
if (announced) {
|
|
383
|
+
const entry = lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
384
|
+
sseEmitter.broadcast({
|
|
385
|
+
type: "subagent",
|
|
386
|
+
data: {
|
|
387
|
+
phase: "dismissed",
|
|
388
|
+
childSessionKey: entry?.childSessionKey ?? announced.childSessionKey,
|
|
389
|
+
runId: entry?.runId ?? announced.bareRunId ?? null,
|
|
390
|
+
parentRunId: entry?.parentRunId ?? null,
|
|
391
|
+
depth: entry?.depth ?? 1,
|
|
392
|
+
deviceId: deviceIdRaw,
|
|
393
|
+
},
|
|
394
|
+
}, deviceIdRaw);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
368
397
|
const subagentEntry = lookupByRunId(evt.runId);
|
|
369
398
|
// Only annotate events that originate from the subagent itself
|
|
370
399
|
// (sessionKey matches childSessionKey). Main-agent delivery events
|
|
@@ -375,6 +404,11 @@ export function forwardAgentEventRaw(evt) {
|
|
|
375
404
|
label: subagentEntry.label,
|
|
376
405
|
parentRunId: subagentEntry.parentRunId,
|
|
377
406
|
depth: subagentEntry.depth,
|
|
407
|
+
// A1: ship the authoritative childSessionKey + lifecycle status on every
|
|
408
|
+
// subagent agent-delta so the app routes/identifies by stable keys instead of
|
|
409
|
+
// guessing from runId.
|
|
410
|
+
childSessionKey: subagentEntry.childSessionKey,
|
|
411
|
+
status: subagentEntry.status,
|
|
378
412
|
}
|
|
379
413
|
: undefined;
|
|
380
414
|
let outgoingData = { ...evt.data };
|
|
@@ -82,8 +82,12 @@ function resolveConfiguredAgents() {
|
|
|
82
82
|
const agents = cfg?.agents;
|
|
83
83
|
const list = agents?.list;
|
|
84
84
|
if (!Array.isArray(list) || list.length === 0) {
|
|
85
|
+
// Implicit `main` agent (no `agents.list`): config carries no name, so fall
|
|
86
|
+
// back to the workspace IDENTITY.md `Name` — the same source ControlUI and
|
|
87
|
+
// the list branch below use — instead of letting the app show the raw id.
|
|
88
|
+
const name = readWorkspaceIdentityName(rt, cfg, DEFAULT_AGENT_ID);
|
|
85
89
|
return {
|
|
86
|
-
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true }],
|
|
90
|
+
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true, ...(name ? { name } : {}) }],
|
|
87
91
|
defaultAgentId: DEFAULT_AGENT_ID,
|
|
88
92
|
};
|
|
89
93
|
}
|
package/package.json
CHANGED
|
@@ -48,7 +48,9 @@ export function registerSessionKeyForRun(sessionKey: string, runId: string): voi
|
|
|
48
48
|
*/
|
|
49
49
|
const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
|
|
50
50
|
|
|
51
|
-
function parseAnnounceRunId(
|
|
51
|
+
export function parseAnnounceRunId(
|
|
52
|
+
runId: string,
|
|
53
|
+
): { childSessionKey: string; bareRunId: string } | null {
|
|
52
54
|
const m = runId.match(ANNOUNCE_RUN_ID_RE);
|
|
53
55
|
if (!m) return null;
|
|
54
56
|
return { childSessionKey: m[1] ?? "", bareRunId: m[2] ?? "" };
|
|
@@ -295,6 +295,9 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
295
295
|
label: "cr",
|
|
296
296
|
parentRunId: mainRunId,
|
|
297
297
|
depth: 1,
|
|
298
|
+
// A1: annotation now ships stable identity (childSessionKey) + lifecycle status.
|
|
299
|
+
childSessionKey: childKey,
|
|
300
|
+
status: "running",
|
|
298
301
|
});
|
|
299
302
|
});
|
|
300
303
|
|
|
@@ -524,6 +527,9 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
524
527
|
label: "reviewer",
|
|
525
528
|
parentRunId: mainRunId,
|
|
526
529
|
depth: 1,
|
|
530
|
+
// A1: annotation now ships stable identity (childSessionKey) + lifecycle status.
|
|
531
|
+
childSessionKey: childKeyA,
|
|
532
|
+
status: "running",
|
|
527
533
|
});
|
|
528
534
|
|
|
529
535
|
// sessions_spawn for B (nested from A's tool call — but parentRunId should come from the context)
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
} from "./agent/run-usage-accumulator.js";
|
|
17
17
|
import { sseEmitter } from "./sse/emitter.js";
|
|
18
18
|
import { toSessionStoreKey } from "./session/session-manager.js";
|
|
19
|
+
import {
|
|
20
|
+
ensureSubagentFromSpawnTool,
|
|
21
|
+
resetForTest as resetSubagentRegistryForTest,
|
|
22
|
+
} from "./agent/subagent-registry.js";
|
|
19
23
|
|
|
20
24
|
describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
21
25
|
const runId = "run-thinking-test";
|
|
@@ -395,3 +399,126 @@ function commonPrefixLen(a: string, b: string): number {
|
|
|
395
399
|
while (i < len && a.charCodeAt(i) === b.charCodeAt(i)) i++;
|
|
396
400
|
return i;
|
|
397
401
|
}
|
|
402
|
+
|
|
403
|
+
// P1 of the subagent streaming redo (see subagent-streaming-redo-plan.md):
|
|
404
|
+
// the plugin ships authoritative correlation keys so the app stops self-deriving identity.
|
|
405
|
+
describe("forwardAgentEventRaw (subagent stable-identity fields: A1/A2/A3)", () => {
|
|
406
|
+
const sessionKey = "agent:main:friday-session-test";
|
|
407
|
+
const deviceId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
|
|
408
|
+
const childKey = "agent:main:subagent:abc";
|
|
409
|
+
|
|
410
|
+
type SubagentBroadcast = { type: string; data: Record<string, unknown> };
|
|
411
|
+
|
|
412
|
+
function subagentBroadcasts(phase: string): Record<string, unknown>[] {
|
|
413
|
+
return (sseEmitter.broadcast as ReturnType<typeof vi.fn>).mock.calls
|
|
414
|
+
.map((c) => c[0] as SubagentBroadcast)
|
|
415
|
+
.filter((m) => m.type === "subagent" && m.data.phase === phase)
|
|
416
|
+
.map((m) => m.data);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
beforeEach(() => {
|
|
420
|
+
sseEmitter.resetForTest();
|
|
421
|
+
resetThinkingStreamAccumStateForTest();
|
|
422
|
+
resetOpenClawRunDeviceMappingForTest();
|
|
423
|
+
resetSubagentRegistryForTest();
|
|
424
|
+
registerFridaySessionDeviceMapping(sessionKey, deviceId);
|
|
425
|
+
vi.spyOn(sseEmitter, "broadcastToRun").mockImplementation(() => {});
|
|
426
|
+
vi.spyOn(sseEmitter, "broadcast").mockImplementation(() => {});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterEach(() => {
|
|
430
|
+
vi.restoreAllMocks();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// A2: spawning carries the spawn tool-call id as a stable correlation key.
|
|
434
|
+
it("spawning event carries the spawn toolCallId", () => {
|
|
435
|
+
forwardAgentEventRaw({
|
|
436
|
+
runId: "parent-run",
|
|
437
|
+
seq: 1,
|
|
438
|
+
stream: "tool",
|
|
439
|
+
sessionKey,
|
|
440
|
+
data: {
|
|
441
|
+
name: "sessions_spawn",
|
|
442
|
+
phase: "start",
|
|
443
|
+
toolCallId: "tc-1",
|
|
444
|
+
args: { taskName: "weather" },
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const spawning = subagentBroadcasts("spawning");
|
|
449
|
+
expect(spawning).toHaveLength(1);
|
|
450
|
+
expect(spawning[0].toolCallId).toBe("tc-1");
|
|
451
|
+
expect(spawning[0].childSessionKey).toBeNull();
|
|
452
|
+
expect(spawning[0].runId).toBeNull();
|
|
453
|
+
expect(spawning[0].label).toBe("weather");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// A1: a subagent's own agent-delta is annotated with childSessionKey + status.
|
|
457
|
+
it("subagent agent-delta annotation includes childSessionKey and status", () => {
|
|
458
|
+
ensureSubagentFromSpawnTool({
|
|
459
|
+
childSessionKey: childKey,
|
|
460
|
+
bareRunId: "sub-bare",
|
|
461
|
+
label: "weather",
|
|
462
|
+
deviceId,
|
|
463
|
+
parentRunId: "parent-run",
|
|
464
|
+
requesterSessionKey: sessionKey,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
forwardAgentEventRaw({
|
|
468
|
+
runId: "sub-bare",
|
|
469
|
+
seq: 1,
|
|
470
|
+
stream: "thinking",
|
|
471
|
+
sessionKey: childKey, // subagent's own event → sessionKey === childSessionKey
|
|
472
|
+
data: { text: "looking up", delta: "looking up" },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
476
|
+
const payload = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
|
|
477
|
+
const subagent = payload.subagent as Record<string, unknown>;
|
|
478
|
+
expect(subagent).toBeDefined();
|
|
479
|
+
expect(subagent.childSessionKey).toBe(childKey);
|
|
480
|
+
expect(subagent.status).toBe("running");
|
|
481
|
+
expect(subagent.label).toBe("weather");
|
|
482
|
+
expect(subagent.parentRunId).toBe("parent-run");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// A3: the parent's announce-summary lifecycle.start emits an explicit dismiss keyed by
|
|
486
|
+
// childSessionKey — replacing the app's fragile announce-runId string parsing.
|
|
487
|
+
it("announce-summary lifecycle.start emits a dismissed subagent event by childSessionKey", () => {
|
|
488
|
+
ensureSubagentFromSpawnTool({
|
|
489
|
+
childSessionKey: childKey,
|
|
490
|
+
bareRunId: "sub-bare",
|
|
491
|
+
label: "weather",
|
|
492
|
+
deviceId,
|
|
493
|
+
parentRunId: "parent-run",
|
|
494
|
+
requesterSessionKey: sessionKey,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const announceRunId = `announce:v1:${childKey}:sub-bare`;
|
|
498
|
+
forwardAgentEventRaw({
|
|
499
|
+
runId: announceRunId,
|
|
500
|
+
seq: 1,
|
|
501
|
+
stream: "lifecycle",
|
|
502
|
+
sessionKey, // parent's sessionKey, not the child's
|
|
503
|
+
data: { phase: "start" },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const dismissed = subagentBroadcasts("dismissed");
|
|
507
|
+
expect(dismissed).toHaveLength(1);
|
|
508
|
+
expect(dismissed[0].childSessionKey).toBe(childKey);
|
|
509
|
+
expect(dismissed[0].runId).toBe("sub-bare");
|
|
510
|
+
expect(dismissed[0].parentRunId).toBe("parent-run");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// A3 must not fire on ordinary (non-announce) lifecycle.start frames.
|
|
514
|
+
it("does not emit dismissed for a normal lifecycle.start", () => {
|
|
515
|
+
forwardAgentEventRaw({
|
|
516
|
+
runId: "parent-run",
|
|
517
|
+
seq: 1,
|
|
518
|
+
stream: "lifecycle",
|
|
519
|
+
sessionKey,
|
|
520
|
+
data: { phase: "start" },
|
|
521
|
+
});
|
|
522
|
+
expect(subagentBroadcasts("dismissed")).toHaveLength(0);
|
|
523
|
+
});
|
|
524
|
+
});
|
package/src/friday-session.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type { FridaySessionUsagePayload } from "./session-usage-snapshot.js";
|
|
|
9
9
|
import { readSessionUsageSnapshotFromStore } from "./session-usage-store.js";
|
|
10
10
|
import {
|
|
11
11
|
lookupByRunId,
|
|
12
|
+
lookupByChildSessionKey,
|
|
13
|
+
parseAnnounceRunId,
|
|
12
14
|
registerSessionKeyForRun,
|
|
13
15
|
registerSpawnIntent,
|
|
14
16
|
consumeSpawnIntent,
|
|
@@ -234,7 +236,13 @@ function completeAgentEventForward(params: {
|
|
|
234
236
|
deviceIdRaw: string;
|
|
235
237
|
outgoingData: Record<string, unknown>;
|
|
236
238
|
isTerminalLifecycle: boolean;
|
|
237
|
-
subagentMeta?: {
|
|
239
|
+
subagentMeta?: {
|
|
240
|
+
label?: string;
|
|
241
|
+
parentRunId?: string;
|
|
242
|
+
depth: number;
|
|
243
|
+
childSessionKey?: string;
|
|
244
|
+
status?: string;
|
|
245
|
+
};
|
|
238
246
|
}): void {
|
|
239
247
|
const { evt, sk, deviceIdRaw, outgoingData, isTerminalLifecycle, subagentMeta } = params;
|
|
240
248
|
|
|
@@ -367,6 +375,10 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
367
375
|
phase: "spawning",
|
|
368
376
|
childSessionKey: null,
|
|
369
377
|
runId: null,
|
|
378
|
+
// A2: the spawn tool-call id is the only stable correlation key before the
|
|
379
|
+
// gateway assigns childSessionKey/runId — the app mints the placeholder window
|
|
380
|
+
// under it, then rekeys to childSessionKey on spawned.
|
|
381
|
+
toolCallId,
|
|
370
382
|
label: intent.label ?? null,
|
|
371
383
|
parentRunId: intent.parentRunId,
|
|
372
384
|
depth: intent.depth,
|
|
@@ -408,6 +420,9 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
408
420
|
phase: "spawned",
|
|
409
421
|
runId: compoundRunId,
|
|
410
422
|
childSessionKey: entry.childSessionKey,
|
|
423
|
+
// A2: echo the spawn toolCallId so the app deterministically links this
|
|
424
|
+
// spawned event to the placeholder window it minted at spawning time.
|
|
425
|
+
toolCallId: toolCallId || null,
|
|
411
426
|
label: entry.label ?? null,
|
|
412
427
|
parentRunId: entry.parentRunId ?? null,
|
|
413
428
|
depth: entry.depth,
|
|
@@ -419,6 +434,33 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
|
|
437
|
+
// Phase 3 (A3): announce-summary delivery to the parent. OpenClaw emits the parent's
|
|
438
|
+
// `lifecycle.start` under the announce compound runId once a subagent's result is being
|
|
439
|
+
// folded back in. We parse the authoritative childSessionKey here (the registry already
|
|
440
|
+
// knows how) and broadcast an explicit `dismissed` subagent event, so the app removes the
|
|
441
|
+
// settled window by childSessionKey instead of re-parsing the announce runId itself.
|
|
442
|
+
if (evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
443
|
+
const announced = parseAnnounceRunId(evt.runId);
|
|
444
|
+
if (announced) {
|
|
445
|
+
const entry =
|
|
446
|
+
lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
447
|
+
sseEmitter.broadcast(
|
|
448
|
+
{
|
|
449
|
+
type: "subagent",
|
|
450
|
+
data: {
|
|
451
|
+
phase: "dismissed",
|
|
452
|
+
childSessionKey: entry?.childSessionKey ?? announced.childSessionKey,
|
|
453
|
+
runId: entry?.runId ?? announced.bareRunId ?? null,
|
|
454
|
+
parentRunId: entry?.parentRunId ?? null,
|
|
455
|
+
depth: entry?.depth ?? 1,
|
|
456
|
+
deviceId: deviceIdRaw,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
deviceIdRaw,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
422
464
|
const subagentEntry = lookupByRunId(evt.runId);
|
|
423
465
|
// Only annotate events that originate from the subagent itself
|
|
424
466
|
// (sessionKey matches childSessionKey). Main-agent delivery events
|
|
@@ -429,6 +471,11 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
429
471
|
label: subagentEntry.label,
|
|
430
472
|
parentRunId: subagentEntry.parentRunId,
|
|
431
473
|
depth: subagentEntry.depth,
|
|
474
|
+
// A1: ship the authoritative childSessionKey + lifecycle status on every
|
|
475
|
+
// subagent agent-delta so the app routes/identifies by stable keys instead of
|
|
476
|
+
// guessing from runId.
|
|
477
|
+
childSessionKey: subagentEntry.childSessionKey,
|
|
478
|
+
status: subagentEntry.status,
|
|
432
479
|
}
|
|
433
480
|
: undefined;
|
|
434
481
|
|
|
@@ -71,6 +71,34 @@ describe("handleAgentsList", () => {
|
|
|
71
71
|
expect(body.agents).toEqual([{ id: "main", isDefault: true }]);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it("resolves the implicit main name from IDENTITY.md when no agents.list exists", async () => {
|
|
75
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "friday-identity-main-"));
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(workspace, "IDENTITY.md"),
|
|
78
|
+
"# IDENTITY.md\n\n- **Name:** F.R.I.D.A.Y\n- **Emoji:** 🌿\n",
|
|
79
|
+
);
|
|
80
|
+
try {
|
|
81
|
+
setFridayAgentForwardRuntime({
|
|
82
|
+
runtime: {
|
|
83
|
+
agent: {
|
|
84
|
+
session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
|
|
85
|
+
resolveAgentWorkspaceDir: () => workspace,
|
|
86
|
+
},
|
|
87
|
+
config: { current: () => ({ agents: { defaults: {} } }) },
|
|
88
|
+
},
|
|
89
|
+
} as any);
|
|
90
|
+
|
|
91
|
+
const res = new MockRes();
|
|
92
|
+
await handleAgentsList(makeReq(AUTH), res as any);
|
|
93
|
+
|
|
94
|
+
const body = JSON.parse(res.body);
|
|
95
|
+
expect(body.defaultAgentId).toBe("main");
|
|
96
|
+
expect(body.agents).toEqual([{ id: "main", name: "F.R.I.D.A.Y", isDefault: true }]);
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
74
102
|
it("lists configured agents with normalized ids and resolved fields", async () => {
|
|
75
103
|
setConfig({
|
|
76
104
|
agents: {
|
|
@@ -106,8 +106,12 @@ function resolveConfiguredAgents(): ResolvedAgents {
|
|
|
106
106
|
const list = agents?.list as Array<Record<string, unknown>> | undefined;
|
|
107
107
|
|
|
108
108
|
if (!Array.isArray(list) || list.length === 0) {
|
|
109
|
+
// Implicit `main` agent (no `agents.list`): config carries no name, so fall
|
|
110
|
+
// back to the workspace IDENTITY.md `Name` — the same source ControlUI and
|
|
111
|
+
// the list branch below use — instead of letting the app show the raw id.
|
|
112
|
+
const name = readWorkspaceIdentityName(rt, cfg, DEFAULT_AGENT_ID);
|
|
109
113
|
return {
|
|
110
|
-
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true }],
|
|
114
|
+
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true, ...(name ? { name } : {}) }],
|
|
111
115
|
defaultAgentId: DEFAULT_AGENT_ID,
|
|
112
116
|
};
|
|
113
117
|
}
|