Skip to content

Commit 05860b3

Browse files
committed
fix(agent): session output json with session_id
1 parent 63ee5aa commit 05860b3

5 files changed

Lines changed: 147 additions & 9 deletions

File tree

packages/commands/src/commands/managed-agent/_engine/session-render.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,29 @@ function renderTerminalStatus(status: string, json: boolean): void {
1515
process.stderr.write(`\n[session ${status}]\n`);
1616
}
1717

18+
/**
19+
* Session identity echoed at the head of the `--output json` envelope so
20+
* callers can read the (possibly just-created) session id from stdout and
21+
* chain `session send/get/events/delete` — without scraping stderr.
22+
* Undefined fields are dropped by JSON.stringify.
23+
*/
24+
export interface SessionRenderContext {
25+
session_id?: string;
26+
provider?: string;
27+
agent?: string;
28+
}
29+
1830
/**
1931
* Consume an SSE stream. Text mode renders live (assistant text → stdout,
2032
* diagnostics → stderr). JSON mode collects every event and emits exactly one
2133
* JSON document at the end — `--output json` guarantees a single valid JSON
22-
* result on stdout (mirrors `text chat --stream --output json`).
34+
* result on stdout (mirrors `text chat --stream --output json`). `context`
35+
* prefixes the envelope with the session identity.
2336
*/
2437
export async function streamAndRenderEvents(
2538
events: AsyncIterable<ProviderSessionEvent>,
2639
json: boolean,
40+
context: SessionRenderContext = {},
2741
): Promise<void> {
2842
const collected: ProviderSessionEvent[] = [];
2943
for await (const event of events) {
@@ -36,7 +50,7 @@ export async function streamAndRenderEvents(
3650
}
3751
if (json) {
3852
process.stdout.write(
39-
`${JSON.stringify({ events: sanitizeSessionEvents(collected) }, null, 2)}\n`,
53+
`${JSON.stringify({ ...context, events: sanitizeSessionEvents(collected) }, null, 2)}\n`,
4054
);
4155
}
4256
}
@@ -59,12 +73,17 @@ function renderEvent(event: ProviderSessionEvent): void {
5973
}
6074
}
6175

62-
/** Render a polled (non-streaming) collected result. */
63-
export function renderCollectedEvents(result: CollectedSessionEvents, json: boolean): void {
76+
/** Render a polled (non-streaming) collected result. `context` prefixes the JSON envelope. */
77+
export function renderCollectedEvents(
78+
result: CollectedSessionEvents,
79+
json: boolean,
80+
context: SessionRenderContext = {},
81+
): void {
6482
if (json) {
6583
process.stdout.write(
6684
`${JSON.stringify(
6785
{
86+
...context,
6887
events: sanitizeSessionEvents(result.result.events),
6988
has_more: result.result.has_more,
7089
next_page: result.result.next_page,

packages/commands/src/commands/managed-agent/session-run.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ export default defineCommand({
6262
usageArgs: "--prompt <text> [--agent <name>] [--no-stream] [--file <path>]",
6363
flags: SESSION_RUN_FLAGS,
6464
exampleArgs: ['--prompt "hello"', '--agent assistant --prompt "summarize this repo"'],
65-
notes: CREDENTIALS_NOTE,
65+
notes: [
66+
...CREDENTIALS_NOTE,
67+
"--output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.",
68+
],
6669
async run(ctx) {
6770
const { settings, flags } = ctx;
6871
const format = detectOutputFormat(settings.output);
@@ -106,11 +109,19 @@ export default defineCommand({
106109
if (flags.noStream) {
107110
const run = await startSessionRunPolling(runtime, flags.prompt, runOptions);
108111
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
109-
renderCollectedEvents(run, asJson);
112+
renderCollectedEvents(run, asJson, {
113+
session_id: run.session.id,
114+
provider: run.provider,
115+
agent: run.agentName,
116+
});
110117
} else {
111118
const run = await startSessionRun(runtime, flags.prompt, runOptions);
112119
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
113-
await streamAndRenderEvents(run.events, asJson);
120+
await streamAndRenderEvents(run.events, asJson, {
121+
session_id: run.session.id,
122+
provider: run.provider,
123+
agent: run.agentName,
124+
});
114125
}
115126
}),
116127
);

packages/commands/src/commands/managed-agent/session-send.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export default defineCommand({
7575
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
7676
provider: flags.provider,
7777
});
78-
renderCollectedEvents(result, asJson);
78+
renderCollectedEvents(result, asJson, {
79+
session_id: flags.sessionId,
80+
});
7981
} else {
8082
const events = await sendSessionMessageStreaming(
8183
runtime,
@@ -85,7 +87,9 @@ export default defineCommand({
8587
provider: flags.provider,
8688
},
8789
);
88-
await streamAndRenderEvents(events, asJson);
90+
await streamAndRenderEvents(events, asJson, {
91+
session_id: flags.sessionId,
92+
});
8993
}
9094
}),
9195
);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
2+
import type { CollectedSessionEvents, ProviderSessionEvent } from "@openagentpack/sdk";
3+
import {
4+
renderCollectedEvents,
5+
streamAndRenderEvents,
6+
} from "../src/commands/managed-agent/_engine/session-render.ts";
7+
8+
/**
9+
* `--output json` 会话信封契约:stdout 恰好一个合法 JSON,且信封头部携带
10+
* session_id / provider / agent —— session run 的调用方必须能从 stdout 拿到
11+
* 新建 Session ID 以继续 send/get/events/delete(不靠刮 stderr)。
12+
*/
13+
14+
let stdoutChunks: string[] = [];
15+
let originalStdoutWrite: typeof process.stdout.write;
16+
17+
beforeEach(() => {
18+
stdoutChunks = [];
19+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
20+
process.stdout.write = ((chunk: string | Uint8Array) => {
21+
stdoutChunks.push(String(chunk));
22+
return true;
23+
}) as typeof process.stdout.write;
24+
});
25+
26+
afterEach(() => {
27+
process.stdout.write = originalStdoutWrite;
28+
});
29+
30+
function capturedJson(): Record<string, unknown> {
31+
// 契约:整个 stdout 拼起来是单个合法 JSON
32+
return JSON.parse(stdoutChunks.join("")) as Record<string, unknown>;
33+
}
34+
35+
async function* fakeEventStream(): AsyncIterable<ProviderSessionEvent> {
36+
yield {
37+
type: "message",
38+
role: "assistant",
39+
content: "hi",
40+
} as ProviderSessionEvent;
41+
yield { type: "status", status: "completed" } as ProviderSessionEvent;
42+
}
43+
44+
function fakeCollected(): CollectedSessionEvents {
45+
return {
46+
terminalStatus: "completed",
47+
result: {
48+
events: [
49+
{
50+
type: "message",
51+
role: "assistant",
52+
content: "hi",
53+
} as ProviderSessionEvent,
54+
],
55+
has_more: false,
56+
next_page: undefined,
57+
},
58+
} as CollectedSessionEvents;
59+
}
60+
61+
test("stream json:信封携带 session_id/provider/agent + events", async () => {
62+
await streamAndRenderEvents(fakeEventStream(), true, {
63+
session_id: "sess_stream",
64+
provider: "bailian",
65+
agent: "assistant",
66+
});
67+
const data = capturedJson();
68+
expect(data.session_id).toBe("sess_stream");
69+
expect(data.provider).toBe("bailian");
70+
expect(data.agent).toBe("assistant");
71+
expect(Array.isArray(data.events)).toBe(true);
72+
expect((data.events as unknown[]).length).toBe(2);
73+
});
74+
75+
test("polling json:信封携带 session_id/provider/agent,并保留 has_more/next_page", () => {
76+
renderCollectedEvents(fakeCollected(), true, {
77+
session_id: "sess_poll",
78+
provider: "claude",
79+
agent: "assistant",
80+
});
81+
const data = capturedJson();
82+
expect(data.session_id).toBe("sess_poll");
83+
expect(data.provider).toBe("claude");
84+
expect(data.agent).toBe("assistant");
85+
expect(data.has_more).toBe(false);
86+
expect(Array.isArray(data.events)).toBe(true);
87+
});
88+
89+
test("json:不传 context 时信封形状不变(无 session_id 键)", () => {
90+
renderCollectedEvents(fakeCollected(), true);
91+
const data = capturedJson();
92+
expect("session_id" in data).toBe(false);
93+
expect(Array.isArray(data.events)).toBe(true);
94+
});
95+
96+
test("text 模式:context 不影响 stdout(仍只输出助手文本)", async () => {
97+
await streamAndRenderEvents(fakeEventStream(), false, {
98+
session_id: "sess_text",
99+
});
100+
const output = stdoutChunks.join("");
101+
expect(output).toBe("hi");
102+
expect(output).not.toContain("sess_text");
103+
});

skills/bailian-cli/reference/managed-agent.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ bl managed-agent session list --all
385385
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
386386
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
387387
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
388+
- --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.
388389

389390
#### Examples
390391

0 commit comments

Comments
 (0)