Skip to content

Commit 0ccbd46

Browse files
authored
Merge pull request #140 from phantom5099/steer-userinput
增加回合中用户输入功能
2 parents 40746c5 + dd4d85d commit 0ccbd46

44 files changed

Lines changed: 1615 additions & 491 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/codingcode/src/agent/agent.ts‎

Lines changed: 177 additions & 151 deletions
Large diffs are not rendered by default.

‎packages/codingcode/src/agent/port.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,16 @@ export interface RunTurnOptions {
1717
agentName?: string;
1818
systemPrompt?: string;
1919
skills?: ReadonlyArray<{ name: string; path: string }>;
20+
/** steer 路径下由前端提供,作为 `PendingUserInput.id` 原样透传 */
21+
inputId?: string;
2022
}
2123

24+
export type RunTurnResult =
25+
| { kind: 'turn'; stream: AsyncGenerator<FrameBody>; sessionId: string }
26+
| { kind: 'queued'; sessionId: string; turnId: number };
27+
2228
export interface AgentShape {
23-
runTurn(
24-
input: IncomingPart[],
25-
opts: RunTurnOptions
26-
): Effect.Effect<
27-
{
28-
stream: AsyncGenerator<FrameBody>;
29-
sessionId: string;
30-
},
31-
AgentError
32-
>;
29+
runTurn(input: IncomingPart[], opts: RunTurnOptions): Effect.Effect<RunTurnResult, AgentError>;
3330
}
3431

3532
export class AgentService extends Context.Tag('AgentService')<AgentService, AgentShape>() {}

‎packages/codingcode/src/context/context.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js';
2121
import { AgentError } from '../util/error.js';
2222
import { ContextService } from './port.js';
2323
import type { CompressResult } from './port.js';
24-
import { EventSinkService } from '../sink/port.js';
24+
import { TurnRegistryService } from '../turn/port.js';
2525

2626
export function transcriptPathFor(ref: SessionRef): string {
2727
return transcriptPathOf(ref.cwd, ref.sessionId, ref.parentSessionId);
@@ -211,7 +211,7 @@ export const ContextLayer = Layer.effect(
211211
Effect.gen(function* () {
212212
const session = yield* SessionService;
213213
const llm = yield* LLMService;
214-
const sink = yield* EventSinkService;
214+
const turn = yield* TurnRegistryService;
215215

216216
// 回合内内存态:键 = sessionId,仅在换回合(turnId 变化)时重建
217217
const buffers = new Map<string, ContextBuffer>();
@@ -429,13 +429,10 @@ export const ContextLayer = Layer.effect(
429429
const contextWindow = contextWindowOf(model);
430430
yield* runMicroCompact(buf, contextWindow);
431431
if (needsCompaction(buf, contextWindow)) {
432-
// 压缩判定与压缩帧都归 context,agent 不参与
433-
yield* sink.emit(ref.sessionId, { family: 'transition', transition: { to: 'compress' } });
432+
// 压缩判定归 context,压缩相位帧由状态表投递
433+
yield* turn.transition(ref.sessionId, { kind: 'compressing' });
434434
yield* summarizeToFit(buf, contextWindow, model);
435-
yield* sink.emit(ref.sessionId, {
436-
family: 'transition',
437-
transition: { to: 'executing' },
438-
});
435+
yield* turn.transition(ref.sessionId, { kind: 'running' });
439436
}
440437
return yield* resolveMediaParts(buildContextMessages(buf.events, buf.compactedTurnIds), ref.cwd);
441438
});

‎packages/codingcode/src/layer.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AgentLayer } from './agent/agent.js';
1717
import { ToolEnvLayer } from './agent/tool-env.js';
1818
import { SubagentRunnerLayer } from './subagent/subagent.js';
1919
import { SubagentRunRegistryLayer } from './subagent/registry.js';
20+
import { TurnRegistryLayer } from './turn/registry.js';
2021
import { MailboxLayer } from './session/mailbox.js';
2122
import { SchedulerLayer } from './scheduler/scheduler.js';
2223

@@ -31,14 +32,17 @@ const InfraLayer = Layer.mergeAll(
3132
TodoLayer
3233
);
3334

35+
// 状态帧由 turn 投递 ⇒ turn 要 sink;
36+
const TurnWithDeps = TurnRegistryLayer.pipe(Layer.provide(EventSinkLayer));
37+
3438
const ApprovalWithDeps = ApprovalLayer.pipe(
3539
Layer.provide(Layer.mergeAll(HookLayer, EventSinkLayer, ApprovalWaitLayer))
3640
);
3741
const ToolExecutorWithDeps = ToolExecutorLayer.pipe(
3842
Layer.provide(Layer.mergeAll(HookLayer, ApprovalWithDeps))
3943
);
4044
const ContextWithDeps = ContextLayer.pipe(
41-
Layer.provide(Layer.mergeAll(SessionLayer, LlmLayer, EventSinkLayer))
45+
Layer.provide(Layer.mergeAll(SessionLayer, LlmLayer, EventSinkLayer, TurnWithDeps))
4246
);
4347
const MemoryWithDeps = MemoryLayer.pipe(Layer.provide(LlmLayer));
4448

@@ -52,7 +56,8 @@ const AgentServiceLayers = Layer.mergeAll(
5256
ContextWithDeps,
5357
MemoryWithDeps,
5458
CheckpointLayer,
55-
LlmLayer
59+
LlmLayer,
60+
TurnWithDeps
5661
);
5762

5863
// agent with deps
@@ -65,8 +70,9 @@ const SubagentWithDeps = SubagentRunnerLayer.pipe(Layer.provide(AgentWithDeps));
6570

6671
// 运行注册表:要 runner 起子代理、要 mailbox 投递终态、要 sink 发 subagent_event 帧、
6772
// 要 hooks 在子代理终态时触发 agent.subagent.complete。
73+
// 终态归属已并入状态表(turn 已在 AgentWithDeps 链路里,Layer 按引用去重 ⇒ 全进程一份 records)。
6874
const SubagentRunRegistryWithDeps = SubagentRunRegistryLayer.pipe(
69-
Layer.provide(Layer.mergeAll(SubagentWithDeps, MailboxLayer, EventSinkLayer, HookLayer))
75+
Layer.provide(Layer.mergeAll(SubagentWithDeps, MailboxLayer, EventSinkLayer, HookLayer, TurnWithDeps))
7076
);
7177

7278
export const AppLayer = Layer.mergeAll(
@@ -82,6 +88,7 @@ export const AppLayer = Layer.mergeAll(
8288
AgentWithDeps,
8389
SubagentWithDeps,
8490
SubagentRunRegistryWithDeps,
91+
TurnWithDeps,
8592
SchedulerLayer,
8693
EventSinkLayer
8794
);

‎packages/codingcode/src/scheduler/scheduler.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const SchedulerLayer = Layer.effect(
4848
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
4949

5050
try {
51-
const { stream, sessionId } = await _rt.runPromise(
51+
const turnResult = await _rt.runPromise(
5252
Effect.gen(function* () {
5353
const agent = yield* AgentService;
5454
return yield* agent.runTurn([textPart(auto.description)], {
@@ -61,6 +61,9 @@ export const SchedulerLayer = Layer.effect(
6161
});
6262
})
6363
);
64+
// 自动化不开已有会话,不可能命中活跃回合
65+
if (turnResult.kind === 'queued') return;
66+
const { stream, sessionId } = turnResult;
6467

6568
let lastContent = '';
6669
for await (const body of stream) {
@@ -181,7 +184,7 @@ export const SchedulerLayer = Layer.effect(
181184
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
182185

183186
try {
184-
const { stream, sessionId } = await _rt.runPromise(
187+
const turnResult = await _rt.runPromise(
185188
Effect.gen(function* () {
186189
const agent = yield* AgentService;
187190
return yield* agent.runTurn([textPart(auto.description)], {
@@ -193,6 +196,9 @@ export const SchedulerLayer = Layer.effect(
193196
});
194197
})
195198
);
199+
// 同上:自动化不开已有会话,queued 不可达
200+
if (turnResult.kind === 'queued') return null;
201+
const { stream, sessionId } = turnResult;
196202

197203
for await (const body of stream) {
198204
if (

‎packages/codingcode/src/server/http-error.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const DOMAIN_STATUS: Record<ErrorCode, number> = {
2121
EMPTY_RESPONSE: 500,
2222
SESSION_IO_ERROR: 500,
2323
INVALID_INPUT: 400,
24+
TURN_CONFLICT: 409,
2425
};
2526

2627
export class NotFoundError extends Error {

‎packages/codingcode/src/server/routes/messages.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ASK_BEFORE_EXEC_PERMISSION_MODE, BUILD_PROFILE_NAME } from '../../util/
1010
import type { IncomingMedia, IncomingPart } from '../../llm/types.js';
1111
import {
1212
frameStream,
13+
json,
1314
pathParams,
1415
readJsonFrom,
1516
sseResponse,
@@ -28,6 +29,8 @@ type MessageBody = {
2829
input: WirePart[];
2930
cwd: string;
3031
model?: string;
32+
/** 前端队列项 id:命中活跃回合时原样回显在 user_input 帧上 */
33+
inputId?: string;
3134
skills?: Array<{ name: string; path: string }>;
3235
};
3336

@@ -94,6 +97,7 @@ const sendMessage: Handler = Effect.gen(function* () {
9497
cwd,
9598
signal: web.signal,
9699
model: body.model,
100+
inputId: body.inputId,
97101
skills: body.skills,
98102
};
99103
if (isNew) {
@@ -107,12 +111,16 @@ const sendMessage: Handler = Effect.gen(function* () {
107111
}
108112

109113
const agent = yield* AgentService;
110-
const { stream, sessionId: actualSid } = yield* agent.runTurn(input, {
114+
const result = yield* agent.runTurn(input, {
111115
sessionId: isNew ? undefined : sessionId,
112116
...runOpts,
113117
} as never);
114118

115-
return yield* sseResponse(frameStream(stream), { sessionId: actualSid });
119+
// 命中活跃回合:输入已进状态表,不开新流,回 202
120+
if (result.kind === 'queued') {
121+
return json({ queued: true, sessionId: result.sessionId, turnId: result.turnId }, 202);
122+
}
123+
return yield* sseResponse(frameStream(result.stream), { sessionId: result.sessionId });
116124
});
117125

118126
export const addMessagesRoutes = (router: Router): Router =>

‎packages/codingcode/src/server/routes/sessions.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { SessionService } from '../../session/port.js';
1313
import { computePaths } from '../../session/paths.js';
1414
import { ContextService } from '../../context/port.js';
1515
import { MailboxService } from '../../session/mailbox.js';
16+
import { TurnRegistryService } from '../../turn/port.js';
1617
import { estimatePromptTokensFrom } from '../../context/context.js';
1718
import { CheckpointService } from '../../checkpoint/port.js';
1819
import { activeModelId, setGlobalActive } from '../../infra/models.js';
@@ -102,9 +103,14 @@ const deleteSession: Handler = Effect.gen(function* () {
102103
const session = yield* SessionService;
103104
const context = yield* ContextService;
104105
const mailbox = yield* MailboxService;
105-
yield* session.deleteSession(id ?? '', cwd);
106-
yield* context.dispose(id ?? '');
107-
yield* mailbox.dispose(id ?? '');
106+
const turn = yield* TurnRegistryService;
107+
const sessionId = id ?? '';
108+
// 先请求停止:子代理纤维仍在跑的话,dispose 后它会成为孤儿(还会 mailbox.offer 重建队列)
109+
yield* turn.stopChildren(sessionId);
110+
yield* session.deleteSession(sessionId, cwd);
111+
yield* context.dispose(sessionId);
112+
yield* mailbox.dispose(sessionId);
113+
yield* turn.dispose(sessionId);
108114
return json({ ok: true });
109115
});
110116

‎packages/codingcode/src/server/routes/subagents.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as HttpRouter from '@effect/platform/HttpRouter';
22
import { Effect } from 'effect';
3-
import { SubagentRunRegistryService } from '../../subagent/registry.js';
3+
import { TurnRegistryService } from '../../turn/port.js';
44
import { json, pathParams, type Handler, type Router } from '../handler.js';
55

66
const stopAll: Handler = Effect.gen(function* () {
77
const { id: sessionId } = yield* pathParams;
8-
const registry = yield* SubagentRunRegistryService;
9-
const stopped = yield* registry.stopAll(sessionId ?? '');
8+
const turn = yield* TurnRegistryService;
9+
const stopped = yield* turn.stopChildren(sessionId ?? '');
1010
return json({ stopped });
1111
});
1212

‎packages/codingcode/src/session/port.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export interface SessionShape {
4848
): Effect.Effect<Map<string, string>, AgentError>;
4949

5050
recordUser(state: SessionStoreState, content: StoredPart[]): Effect.Effect<UserEvent, AgentError>;
51+
/** steer 输入:归属当前回合,不递增 turnId、不改标题 */
52+
recordUserInput(
53+
state: SessionStoreState,
54+
content: StoredPart[]
55+
): Effect.Effect<UserEvent, AgentError>;
5156
recordSystem(
5257
state: SessionStoreState,
5358
content: StoredPart[]

0 commit comments

Comments
 (0)