refactor: unify agent and session identity
This commit is contained in:
@@ -11,11 +11,11 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
|
||||
| Plugin | Why it is here |
|
||||
|---|---|
|
||||
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
|
||||
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model` with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating one agent under the `main` config label from this app's `model`, with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
|
||||
| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
|
||||
| `stdio-chat` (in-package module) | the readline UI, holding the app-owned agent object directly and rendering it as `main` |
|
||||
|
||||
`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in could never carry the per-file coverage gate). Unlike the console logger, a stray `hmr` is not a stdout-purity footgun, so leaving it at the leaf costs no safety. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`.
|
||||
|
||||
@@ -25,14 +25,14 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
|
||||
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `model` | (required) | the pre-created `main` agent's model |
|
||||
| `model` | (required) | the pre-created agent's model |
|
||||
| `persona` | — | the deployment persona template (may reference `{{model}}`), routed to `dsh-system-prompt` |
|
||||
| `toolOrder` | — | explicit model-facing tool order (a name list with one `'<unlisted-tools>'` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` |
|
||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||
| `welcome` | `ready.` | the stdin-chat banner |
|
||||
| `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) |
|
||||
|
||||
Fresh stdio sessions use the process launch directory as `session.header.cwd`, so project-scoped features such as skill discovery and default bash workdir follow the directory where `dsh-stdio-agent` was started. Resumed sessions keep the cwd stored in the persisted session header.
|
||||
Fresh stdio sessions use the process launch directory as `session.header.cwd` and mint one combined `main-session-<uuid>` agent/session id, so durable restarts cannot collide. The UI's `main` text is a display label, not a second routing id. Resumed sessions register under the exact `resumeSessionId` and keep the cwd stored in the persisted session header.
|
||||
|
||||
## The bin
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster a terminal
|
||||
* chat needs — a console logger, the readline UI (the in-package `stdio-chat`
|
||||
* module), JSONL session
|
||||
* persistence, and a pre-created `main` agent the UI drives.
|
||||
* persistence, and one pre-created agent the UI drives under its `main` label.
|
||||
*
|
||||
* The cluster is BAKED IN, not left to the leaf: a stdio app always logs to the
|
||||
* console (stdout is just the terminal) and always pre-creates the `main` agent
|
||||
* the readline UI sends to. The leaf supplies the swappable backends (the LLM
|
||||
* console (stdout is just the terminal) and always pre-creates one agent the
|
||||
* readline UI labels `main`. The leaf supplies the swappable backends (the LLM
|
||||
* adapter, the bash executor), optional product tools, the optional `hmr`
|
||||
* dev-reload plugin, and this app's {@link Config} (model, prompt, persistence
|
||||
* root, welcome banner).
|
||||
@@ -41,7 +41,6 @@
|
||||
import type { Context } from 'cordis'
|
||||
import ConsoleExporter from '@cordisjs/plugin-logger-console'
|
||||
import z from 'schemastery'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import * as agentCore from '@deepseek-ai/dsh-agent-core'
|
||||
@@ -54,7 +53,7 @@ export const name = 'stdio-agent'
|
||||
|
||||
/**
|
||||
* App config: the swappable per-demo values, each routed to where the app wires
|
||||
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
|
||||
* it. `model`/`resumeSessionId` configure the pre-created agent (through
|
||||
* {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list); `persona` is
|
||||
* the deployment persona (forwarded to the system-prompt plugin); `toolOrder`
|
||||
* is the explicit model-facing tool order (forwarded to the system-prompt plugin);
|
||||
@@ -63,7 +62,7 @@ export const name = 'stdio-agent'
|
||||
* `welcome` is the UI banner.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Model name for the `main` agent (must have a registered adapter). */
|
||||
/** Model name for the pre-created agent (must have a registered adapter). */
|
||||
model: string
|
||||
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
||||
persona?: string
|
||||
@@ -78,7 +77,7 @@ export interface Config {
|
||||
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
|
||||
skills?: agentCore.SkillConfig
|
||||
/**
|
||||
* If set, the `main` agent RESUMES this persisted session id instead of
|
||||
* If set, the pre-created agent RESUMES this persisted session id instead of
|
||||
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
|
||||
* (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
|
||||
*/
|
||||
@@ -103,9 +102,9 @@ export const Config: z<Config> = z.object({
|
||||
|
||||
/**
|
||||
* Compose the spine with the stdio front door. The console logger comes first
|
||||
* (infra), then the agent-core bundle pre-creating the `main` agent from this
|
||||
* app's `model`/`resumeSessionId` with the deployment `persona`, then the JSONL
|
||||
* backend, then the readline UI bound to `main`. The `hmr` dev-reload plugin is
|
||||
* (infra), then the agent-core bundle pre-creating one agent from this app's
|
||||
* `model`/`resumeSessionId` with the deployment `persona`, then the JSONL
|
||||
* backend, then the readline UI rendering that object as `main`. The `hmr` dev-reload plugin is
|
||||
* a leaf concern (see the module doc), so it is not mounted here.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
@@ -115,7 +114,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
|
||||
...config.tools !== undefined ? { tools: config.tools } : {},
|
||||
agents: [{
|
||||
id: AgentId('main'),
|
||||
id: 'main',
|
||||
model: config.model,
|
||||
cwd: process.cwd(),
|
||||
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
||||
@@ -125,5 +124,5 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
|
||||
ctx.plugin(UserInteractionService)
|
||||
ctx.plugin(toolAskUser)
|
||||
ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' })
|
||||
ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.' })
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { createInterface } from 'node:readline'
|
||||
import type { Readable, Writable } from 'node:stream'
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import {
|
||||
UserInteractionError,
|
||||
type AskUserQuestionAnswer,
|
||||
@@ -36,15 +36,10 @@ export const inject = ['agents', 'userInteraction']
|
||||
export interface Config {
|
||||
/** Banner printed once on start, before the first `> ` prompt. */
|
||||
welcome?: string
|
||||
// TODO(fixed-stdio-agent): this app-internal plugin is mounted only for the
|
||||
// precreated `main` agent; remove configurability and its config-only test.
|
||||
/** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
|
||||
agent?: string
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
welcome: z.string().default('ready.'),
|
||||
agent: z.string().default('main'),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -98,23 +93,16 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
// Loader validation, so it must be self-contained rather than trusting the
|
||||
// cast — `config.welcome as string` would otherwise be `undefined` on `{}`.
|
||||
const welcome = config.welcome ?? 'ready.'
|
||||
const agentId = AgentId(config.agent ?? 'main')
|
||||
const { input, output, exit } = runtime
|
||||
|
||||
// Render label lookup: the `turn/start` session event carries only the turn
|
||||
// number, so to print the short agent id (`[main turn 1]`) we map the
|
||||
// session's id to its agent's id. The session id is not reliably the agent id
|
||||
// (a session can be created with an explicit/client-supplied id), so build the
|
||||
// map from `agent/created` rather than parsing the id string. Seed from the
|
||||
// registry's current agents first: an agent registered before this plugin
|
||||
// installed (e.g. the pre-created `main` agent, or any agent surviving an HMR
|
||||
// reload of just this fiber) already fired its `agent/created`, so the live
|
||||
// listener alone would miss it and its turns would fall back to the raw
|
||||
// session id.
|
||||
const labelBySession = new Map<string, string>()
|
||||
for (const agent of ctx.agents.list()) labelBySession.set(agent.session.header.id, agent.id)
|
||||
ctx.on('agent/created', (agent) => { labelBySession.set(agent.session.header.id, agent.id) })
|
||||
ctx.on('agent/disposed', (agent) => { labelBySession.delete(agent.session.header.id) })
|
||||
// This app owns exactly one pre-created agent. Hold the live object directly:
|
||||
// its per-run id is intentionally fresh, while `main` remains only the
|
||||
// terminal's fixed display label.
|
||||
let target: Agent | undefined = ctx.agents.list()[0]
|
||||
ctx.on('agent/created', (agent) => { target ??= agent })
|
||||
ctx.on('agent/disposed', (agent) => {
|
||||
if (target === agent) target = undefined
|
||||
})
|
||||
|
||||
// Transcript rendering off the durable `session/event` feed — the assistant
|
||||
// token stream, turn/step boundaries, tool activity, and todos all come from
|
||||
@@ -136,7 +124,7 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
output.write(chunk.text)
|
||||
}
|
||||
} else if (event.type === 'turn/start') {
|
||||
const label = labelBySession.get(session.header.id) ?? session.header.id
|
||||
const label = target?.session === session ? 'main' : session.id
|
||||
output.write(`\n[${label} turn ${event.data.turn}] `)
|
||||
} else if (event.type === 'turn/end') {
|
||||
if (inReasoning) output.write('\x1B[0m')
|
||||
@@ -187,7 +175,7 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
// Work submitted: wait until a turn has run and the agent is idle.
|
||||
if (submittedWork) {
|
||||
if (!sawRunning) return
|
||||
const agent = ctx.agents.get(agentId)
|
||||
const agent = target
|
||||
if (agent && agent.status !== 'idle') return // a turn is still running
|
||||
}
|
||||
// Let any final output flush, then exit. The handle is tracked so the
|
||||
@@ -201,7 +189,7 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
}
|
||||
|
||||
const disposeStatusListener = ctx.on('agent/status', (subject, status) => {
|
||||
if (subject.id !== agentId) return
|
||||
if (subject !== target) return
|
||||
if (status === 'running') sawRunning = true
|
||||
if (status === 'idle') maybeExit()
|
||||
})
|
||||
@@ -354,9 +342,9 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
}
|
||||
const text = line.trim()
|
||||
if (!text) return
|
||||
const agent = ctx.agents.get(agentId)
|
||||
const agent = target
|
||||
if (!agent) {
|
||||
ctx.logger.error('ui-stdio: agent "%s" is not running', agentId)
|
||||
ctx.logger.error('ui-stdio: main agent is not running')
|
||||
return
|
||||
}
|
||||
submittedWork = true
|
||||
|
||||
@@ -16,7 +16,7 @@ function fakeContext(): Context {
|
||||
return {
|
||||
on: vi.fn(() => vi.fn()),
|
||||
effect: vi.fn((callback: () => () => void) => callback()),
|
||||
// The UI seeds its label map from the registry at install; this suite only
|
||||
// The UI seeds its target object from the registry at install; this suite only
|
||||
// exercises readline terminal-mode selection, so an empty roster suffices.
|
||||
agents: { list: vi.fn(() => []) },
|
||||
userInteraction: { registerProvider: vi.fn(() => vi.fn()) },
|
||||
|
||||
@@ -4,7 +4,8 @@ import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { AgentId, agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import * as stdioAgent from '../src/index.ts'
|
||||
@@ -82,9 +83,12 @@ describe('dsh-stdio-agent app', () => {
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('userInteraction')).toBeDefined()
|
||||
expect(ctx.get('tools')?.get('ask_user_question')).toBeDefined()
|
||||
// The pre-created `main` agent the UI drives.
|
||||
const agent = ctx.get('agents')?.get(AgentId('main'))
|
||||
// The sole pre-created agent the UI drives. `main` is its stable config
|
||||
// label; each fresh process mints a durable combined agent/session id.
|
||||
const agent = ctx.get('agents')?.list()[0]
|
||||
expect(agent).toBeDefined()
|
||||
expect(agent?.id).toBe(agent?.session.id)
|
||||
expect(agent?.id).toMatch(/^main-session-/)
|
||||
expect(agent?.session.header.cwd).toBe(process.cwd())
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
@@ -99,7 +103,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
stdioAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
expect(ctx.get('agents')?.list()).toHaveLength(1)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -116,7 +120,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
|
||||
it('forwards resumeSessionId onto the pre-created agent when set', async () => {
|
||||
// A resume id defers agent creation until persistence loads; with no backing
|
||||
// session the resume is contained + logged, so no `main` agent registers —
|
||||
// session the resume is contained + logged, so no agent registers —
|
||||
// the branch that maps resumeSessionId through is what this covers.
|
||||
const ctx = await mount({
|
||||
model: 'mock',
|
||||
@@ -125,7 +129,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
resumeSessionId: 'no-such-session',
|
||||
skills: await isolatedSkillsConfig(),
|
||||
})
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
|
||||
expect(ctx.get('agents')?.list()).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -57,17 +57,16 @@ function makeAgent(id: string, status: AgentStatus = 'idle'): Agent & {
|
||||
status,
|
||||
sent,
|
||||
steered,
|
||||
// A minimal session stub: the UI reads only `session.header.id` (to map the
|
||||
// session back to its agent id for the turn-boundary label).
|
||||
session: { header: { id: `${id}-session` } },
|
||||
// A minimal session stub with the agent's shared durable identity.
|
||||
session: { id, header: { id } },
|
||||
send: (content: ContentBlock[]) => void sent.push(content),
|
||||
steer: (content: ContentBlock[]) => void steered.push(content),
|
||||
} as never
|
||||
}
|
||||
|
||||
/** A session stub whose `header.id` matches an agent's, for `session/event` emits. */
|
||||
function makeSession(agentId: string): Session {
|
||||
return { header: { id: `${agentId}-session` } } as Session
|
||||
function makeSession(id: string): Session {
|
||||
return { id, header: { id } } as Session
|
||||
}
|
||||
|
||||
/** An `assistant/chunk` session event carrying one raw stream chunk. */
|
||||
@@ -75,7 +74,7 @@ function chunkEvent(chunk: StreamChunk): SessionEvent {
|
||||
return { type: 'assistant/chunk', seq: 0, time: 0, data: { turn: 1, step: 0, chunk } }
|
||||
}
|
||||
|
||||
const CONFIG: Config = { welcome: 'hi there', agent: 'main' }
|
||||
const CONFIG: Config = { welcome: 'hi there' }
|
||||
|
||||
async function setup(config: Config = CONFIG, runtimeOver: Partial<StdioRuntime> = {}) {
|
||||
const ctx = new Context()
|
||||
@@ -99,12 +98,11 @@ describe('createStdioChat rendering', () => {
|
||||
expect(out.text()).toBe('hi there\n> ')
|
||||
})
|
||||
|
||||
it('falls back to default welcome/agent when called with empty config', async () => {
|
||||
it('falls back to the default welcome when called with empty config', async () => {
|
||||
// createStdioChat is exported and may be driven directly (bypassing the
|
||||
// Loader's schemastery validation), so it must default welcome/agent itself.
|
||||
// Loader's schemastery validation), so it must default the welcome itself.
|
||||
const { out } = await setup({})
|
||||
expect(out.text()).toBe('ready.\n> ')
|
||||
// And it drives the default agent id 'main'.
|
||||
})
|
||||
|
||||
it('detects readline terminal mode from both stream TTY flags', async () => {
|
||||
@@ -156,9 +154,9 @@ describe('createStdioChat rendering', () => {
|
||||
it('renders turn/start and turn/end markers from the session feed', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
const agent = makeAgent('main')
|
||||
// agent/created populates the session-id → agent-id label map.
|
||||
// agent/created supplies the app-owned target object.
|
||||
ctx.emit('agent/created', agent)
|
||||
const session = makeSession('main')
|
||||
const session = agent.session
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/start', seq: 1, time: 0, data: { turn: 3, trigger: { kind: 'message' } },
|
||||
} as SessionEvent)
|
||||
@@ -169,21 +167,20 @@ describe('createStdioChat rendering', () => {
|
||||
expect(out.text()).toContain('\n> ')
|
||||
})
|
||||
|
||||
it('falls back to the session id as the label when no agent is mapped', async () => {
|
||||
it('uses the session id as the label for a non-target session', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
// No agent/created emitted, so the label map is empty — the header id shows.
|
||||
// No target exists, so the event's durable identity is the label.
|
||||
ctx.emit('session/event', makeSession('orphan'), {
|
||||
type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[orphan-session turn 1] ')
|
||||
expect(out.text()).toContain('[orphan turn 1] ')
|
||||
})
|
||||
|
||||
it('seeds labels for agents already registered before the UI installs', async () => {
|
||||
it('uses an agent already registered before the UI installs as its target', async () => {
|
||||
// The pre-created `main` agent (and any agent surviving an HMR reload of just
|
||||
// this fiber) fired its `agent/created` before the UI's listener existed, so
|
||||
// the live listener alone would miss it. Seeding from `ctx.agents.list()` at
|
||||
// install time is what keeps its turn header showing `[main turn N]` instead
|
||||
// of the raw session id.
|
||||
// install time preserves the terminal's fixed `[main turn N]` label.
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
@@ -193,7 +190,7 @@ describe('createStdioChat rendering', () => {
|
||||
await ctx.plugin(Object.assign((inner: Context) => {
|
||||
createStdioChat(inner, CONFIG, runtime)
|
||||
}, { inject: ['agents', 'userInteraction'] }))
|
||||
ctx.emit('session/event', makeSession('main'), {
|
||||
ctx.emit('session/event', agent.session, {
|
||||
type: 'turn/start', seq: 1, time: 0, data: { turn: 5, trigger: { kind: 'message' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[main turn 5] ')
|
||||
@@ -209,17 +206,28 @@ describe('createStdioChat rendering', () => {
|
||||
expect(out.text()).toContain('\x1B[2mmid\x1B[0m')
|
||||
})
|
||||
|
||||
it('drops the label mapping on agent/disposed', async () => {
|
||||
it('drops the target object on agent/disposed', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
const agent = makeAgent('main')
|
||||
ctx.emit('agent/created', agent)
|
||||
ctx.emit('agent/disposed', agent)
|
||||
// After disposal the map no longer resolves the agent id — fall back to the
|
||||
// session header id.
|
||||
ctx.emit('session/event', makeSession('main'), {
|
||||
// After disposal the event belongs to a non-target session, so its durable
|
||||
// identity is rendered directly.
|
||||
ctx.emit('session/event', agent.session, {
|
||||
type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[main-session turn 1] ')
|
||||
expect(out.text()).toContain('[main turn 1] ')
|
||||
})
|
||||
|
||||
it('keeps the target when a different agent is disposed', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
const target = makeAgent('target')
|
||||
ctx.emit('agent/created', target)
|
||||
ctx.emit('agent/disposed', makeAgent('other'))
|
||||
ctx.emit('session/event', target.session, {
|
||||
type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[main turn 1] ')
|
||||
})
|
||||
|
||||
it('renders tool/call and tool/result session events', async () => {
|
||||
@@ -666,11 +674,11 @@ describe('createStdioChat input', () => {
|
||||
const spy = vi.spyOn(ctx.logger, 'error').mockImplementation(() => {})
|
||||
input.feed('nobody home')
|
||||
await new Promise(r => setImmediate(r))
|
||||
expect(spy).toHaveBeenCalledWith('ui-stdio: agent "%s" is not running', 'main')
|
||||
expect(spy).toHaveBeenCalledWith('ui-stdio: main agent is not running')
|
||||
})
|
||||
|
||||
it('drives the agent named in config, not a hardcoded id', async () => {
|
||||
const { ctx, input } = await setup({ welcome: 'w', agent: 'worker' })
|
||||
it('drives the app-owned agent without a duplicate id config', async () => {
|
||||
const { ctx, input } = await setup({ welcome: 'w' })
|
||||
const agent = makeAgent('worker')
|
||||
ctx.agents.register(agent)
|
||||
input.feed('hi')
|
||||
|
||||
Reference in New Issue
Block a user