mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(agent): session output json with session_id
This commit is contained in:
@@ -15,15 +15,29 @@ function renderTerminalStatus(status: string, json: boolean): void {
|
||||
process.stderr.write(`\n[session ${status}]\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Session identity echoed at the head of the `--output json` envelope so
|
||||
* callers can read the (possibly just-created) session id from stdout and
|
||||
* chain `session send/get/events/delete` — without scraping stderr.
|
||||
* Undefined fields are dropped by JSON.stringify.
|
||||
*/
|
||||
export interface SessionRenderContext {
|
||||
session_id?: string;
|
||||
provider?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume an SSE stream. Text mode renders live (assistant text → stdout,
|
||||
* diagnostics → stderr). JSON mode collects every event and emits exactly one
|
||||
* JSON document at the end — `--output json` guarantees a single valid JSON
|
||||
* result on stdout (mirrors `text chat --stream --output json`).
|
||||
* result on stdout (mirrors `text chat --stream --output json`). `context`
|
||||
* prefixes the envelope with the session identity.
|
||||
*/
|
||||
export async function streamAndRenderEvents(
|
||||
events: AsyncIterable<ProviderSessionEvent>,
|
||||
json: boolean,
|
||||
context: SessionRenderContext = {},
|
||||
): Promise<void> {
|
||||
const collected: ProviderSessionEvent[] = [];
|
||||
for await (const event of events) {
|
||||
@@ -36,7 +50,7 @@ export async function streamAndRenderEvents(
|
||||
}
|
||||
if (json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ events: sanitizeSessionEvents(collected) }, null, 2)}\n`,
|
||||
`${JSON.stringify({ ...context, events: sanitizeSessionEvents(collected) }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -59,12 +73,17 @@ function renderEvent(event: ProviderSessionEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Render a polled (non-streaming) collected result. */
|
||||
export function renderCollectedEvents(result: CollectedSessionEvents, json: boolean): void {
|
||||
/** Render a polled (non-streaming) collected result. `context` prefixes the JSON envelope. */
|
||||
export function renderCollectedEvents(
|
||||
result: CollectedSessionEvents,
|
||||
json: boolean,
|
||||
context: SessionRenderContext = {},
|
||||
): void {
|
||||
if (json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
...context,
|
||||
events: sanitizeSessionEvents(result.result.events),
|
||||
has_more: result.result.has_more,
|
||||
next_page: result.result.next_page,
|
||||
|
||||
@@ -62,7 +62,10 @@ export default defineCommand({
|
||||
usageArgs: "--prompt <text> [--agent <name>] [--no-stream] [--file <path>]",
|
||||
flags: SESSION_RUN_FLAGS,
|
||||
exampleArgs: ['--prompt "hello"', '--agent assistant --prompt "summarize this repo"'],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
"--output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -106,11 +109,19 @@ export default defineCommand({
|
||||
if (flags.noStream) {
|
||||
const run = await startSessionRunPolling(runtime, flags.prompt, runOptions);
|
||||
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
|
||||
renderCollectedEvents(run, asJson);
|
||||
renderCollectedEvents(run, asJson, {
|
||||
session_id: run.session.id,
|
||||
provider: run.provider,
|
||||
agent: run.agentName,
|
||||
});
|
||||
} else {
|
||||
const run = await startSessionRun(runtime, flags.prompt, runOptions);
|
||||
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
|
||||
await streamAndRenderEvents(run.events, asJson);
|
||||
await streamAndRenderEvents(run.events, asJson, {
|
||||
session_id: run.session.id,
|
||||
provider: run.provider,
|
||||
agent: run.agentName,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -75,7 +75,9 @@ export default defineCommand({
|
||||
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
|
||||
provider: flags.provider,
|
||||
});
|
||||
renderCollectedEvents(result, asJson);
|
||||
renderCollectedEvents(result, asJson, {
|
||||
session_id: flags.sessionId,
|
||||
});
|
||||
} else {
|
||||
const events = await sendSessionMessageStreaming(
|
||||
runtime,
|
||||
@@ -85,7 +87,9 @@ export default defineCommand({
|
||||
provider: flags.provider,
|
||||
},
|
||||
);
|
||||
await streamAndRenderEvents(events, asJson);
|
||||
await streamAndRenderEvents(events, asJson, {
|
||||
session_id: flags.sessionId,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
|
||||
import type { CollectedSessionEvents, ProviderSessionEvent } from "@openagentpack/sdk";
|
||||
import {
|
||||
renderCollectedEvents,
|
||||
streamAndRenderEvents,
|
||||
} from "../src/commands/managed-agent/_engine/session-render.ts";
|
||||
|
||||
/**
|
||||
* `--output json` 会话信封契约:stdout 恰好一个合法 JSON,且信封头部携带
|
||||
* session_id / provider / agent —— session run 的调用方必须能从 stdout 拿到
|
||||
* 新建 Session ID 以继续 send/get/events/delete(不靠刮 stderr)。
|
||||
*/
|
||||
|
||||
let stdoutChunks: string[] = [];
|
||||
let originalStdoutWrite: typeof process.stdout.write;
|
||||
|
||||
beforeEach(() => {
|
||||
stdoutChunks = [];
|
||||
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
stdoutChunks.push(String(chunk));
|
||||
return true;
|
||||
}) as typeof process.stdout.write;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
});
|
||||
|
||||
function capturedJson(): Record<string, unknown> {
|
||||
// 契约:整个 stdout 拼起来是单个合法 JSON
|
||||
return JSON.parse(stdoutChunks.join("")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function* fakeEventStream(): AsyncIterable<ProviderSessionEvent> {
|
||||
yield {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "hi",
|
||||
} as ProviderSessionEvent;
|
||||
yield { type: "status", status: "completed" } as ProviderSessionEvent;
|
||||
}
|
||||
|
||||
function fakeCollected(): CollectedSessionEvents {
|
||||
return {
|
||||
terminalStatus: "completed",
|
||||
result: {
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "hi",
|
||||
} as ProviderSessionEvent,
|
||||
],
|
||||
has_more: false,
|
||||
next_page: undefined,
|
||||
},
|
||||
} as CollectedSessionEvents;
|
||||
}
|
||||
|
||||
test("stream json:信封携带 session_id/provider/agent + events", async () => {
|
||||
await streamAndRenderEvents(fakeEventStream(), true, {
|
||||
session_id: "sess_stream",
|
||||
provider: "bailian",
|
||||
agent: "assistant",
|
||||
});
|
||||
const data = capturedJson();
|
||||
expect(data.session_id).toBe("sess_stream");
|
||||
expect(data.provider).toBe("bailian");
|
||||
expect(data.agent).toBe("assistant");
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
expect((data.events as unknown[]).length).toBe(2);
|
||||
});
|
||||
|
||||
test("polling json:信封携带 session_id/provider/agent,并保留 has_more/next_page", () => {
|
||||
renderCollectedEvents(fakeCollected(), true, {
|
||||
session_id: "sess_poll",
|
||||
provider: "claude",
|
||||
agent: "assistant",
|
||||
});
|
||||
const data = capturedJson();
|
||||
expect(data.session_id).toBe("sess_poll");
|
||||
expect(data.provider).toBe("claude");
|
||||
expect(data.agent).toBe("assistant");
|
||||
expect(data.has_more).toBe(false);
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
});
|
||||
|
||||
test("json:不传 context 时信封形状不变(无 session_id 键)", () => {
|
||||
renderCollectedEvents(fakeCollected(), true);
|
||||
const data = capturedJson();
|
||||
expect("session_id" in data).toBe(false);
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
});
|
||||
|
||||
test("text 模式:context 不影响 stdout(仍只输出助手文本)", async () => {
|
||||
await streamAndRenderEvents(fakeEventStream(), false, {
|
||||
session_id: "sess_text",
|
||||
});
|
||||
const output = stdoutChunks.join("");
|
||||
expect(output).toBe("hi");
|
||||
expect(output).not.toContain("sess_text");
|
||||
});
|
||||
@@ -385,6 +385,7 @@ bl managed-agent session list --all
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
Reference in New Issue
Block a user