File size: 6,585 Bytes
68d7816
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { createDecorator } from '#/_base/di/instantiation';
import type { IDisposable } from '#/_base/di/lifecycle';
import { Error2, isError2, type Error2Options } from '#/_base/errors/errors';
import type { ContextMessage } from '#/agent/contextMemory/types';
import type { FinishReason } from '#human/llm/finish-reason';
import type { ContentPart } from '#human/llm/message';
import type { TokenUsage } from '#human/llm/usage';
import type { Hooks } from '#/hooks';
import type { UserEntry } from '#human/agent/turn';
import { LoopErrors } from './errors';
import type {
  MachineEngine,
  MachineEngineAttachBundle,
  MachineEngineAttachRef,
  MachineEngineRetrySnapshot,
  MachineEngineToolCallSnapshot,
} from './machine/engine';

export interface AgentActivityTurnSnapshot {
  readonly turnId: number;
  readonly phase: 'running' | 'tool_call' | 'retrying';
  readonly step: number;
  readonly ending: boolean;
  readonly endingReason?: 'aborted';
  readonly retry?: MachineEngineRetrySnapshot;
  readonly activeToolCalls: readonly MachineEngineToolCallSnapshot[];
  readonly since?: number;
}

export interface AgentActivitySnapshot {
  readonly turn?: AgentActivityTurnSnapshot;
}

export interface LoopSnapshot {
  readonly state: 'idle' | 'running';
  readonly activeTurnId?: number;
  readonly activePromptId?: string;
  readonly queue: readonly UserEntry[];
  readonly notificationCount: number;
  readonly paused: boolean;
  readonly hasPendingRequests: boolean;
  readonly turn?: AgentActivityTurnSnapshot;
  readonly activeTraceId?: string;
}

export type LoopErrorCode = (typeof LoopErrors.codes)[keyof typeof LoopErrors.codes];

export class LoopError extends Error2 {
  constructor(code: LoopErrorCode, message: string, options?: Error2Options) {
    super(code, message, options);
    this.name = 'LoopError';
  }
}

export function createMaxStepsExceededError(maxSteps: number, message?: string): LoopError {
  return new LoopError(
    LoopErrors.codes.LOOP_MAX_STEPS_EXCEEDED,
    message ??
      `Turn exceeded maxSteps=${maxSteps}. If max_steps_per_turn is too small, raise it in config.toml (loop_control.max_steps_per_turn), or run "/update-config" to update it, then "/reload".`,
    { details: { maxSteps } },
  );
}

export function isMaxStepsExceededError(error: unknown): boolean {
  return isError2(error) && error.code === LoopErrors.codes.LOOP_MAX_STEPS_EXCEEDED;
}

export interface BeforeStepContext {
  readonly turnId: number;
  readonly step: number;
  readonly firstStepOfTurn: boolean;
  readonly signal: AbortSignal;
}

export interface AfterStepContext extends BeforeStepContext {
  readonly usage: TokenUsage;
  readonly finishReason: FinishReason;
  stopTurn: boolean;
}

export interface LoopErrorContext {
  readonly turnId: number;
  readonly step?: number;
  readonly stepId?: string;
  readonly signal: AbortSignal;
  readonly error: unknown;
  retry(): void;
}

export interface LoopErrorHandler {
  readonly id: string;
  match(context: LoopErrorContext): boolean;
  handle(context: LoopErrorContext): Promise<boolean | undefined>;
}

export interface LoopErrorHandlerRegistrationOptions {
  readonly before?: string;
  readonly after?: string;
}

export type LoopRunResult =
  | {
      readonly type: 'completed';
      readonly steps: number;
      readonly truncated: boolean;
      readonly stopReason?: string;
    }
  | {
      readonly type: 'failed';
      readonly steps: number;
      readonly error: unknown;
    }
  | {
      readonly type: 'cancelled';
      readonly steps: number;
      readonly reason: unknown;
    };

export type TurnResult = LoopRunResult;

export interface Turn {
  readonly id?: number;
  readonly state?: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
  readonly signal: AbortSignal;
  readonly ready: Promise<void>;
  readonly result: Promise<LoopRunResult>;
  cancel(reason?: unknown): boolean;
}

export interface LoopSubmitOptions {
  readonly steerIfActive?: boolean;
  readonly onMaterialize?: () => void;
}

export interface LoopSubmitResult {
  readonly id: string;
}

export interface LoopCancelTarget {
  readonly turnId?: number;
  readonly promptId?: string;
}

export type PromptState =
  | 'pending'
  | 'running'
  | 'steered'
  | 'completed'
  | 'failed'
  | 'cancelled'
  | 'blocked';

export interface PromptCompletion {
  readonly promptId: string;
  readonly result: TurnResult | undefined;
  readonly state: Extract<PromptState, 'completed' | 'failed' | 'cancelled' | 'blocked'>;
}

export interface PromptSnapshot {
  readonly id: string;
  readonly userMessageId: string;
  readonly createdAt: string;
  readonly state: PromptState;
  readonly message: ContextMessage;
}

export interface PromptHandle extends PromptSnapshot {
  readonly launched: Promise<Turn | undefined>;
  readonly completion: Promise<PromptCompletion>;
}

export interface PromptPayload {
  readonly input: readonly ContentPart[];
  readonly promptId?: string;
}

export interface SteerPayload {
  readonly input: readonly ContentPart[];
}

export interface PromptLaunchResult {
  readonly turn_id: number;
}

export interface PromptSubmitContext {
  readonly promptMessage: ContextMessage;
  readonly isSteer: boolean;
  block: boolean;
}

export interface LoopNotify {
  readonly message?: ContextMessage;
  readonly turnScoped?: boolean;
  readonly bypassMaxSteps?: boolean;
  readonly onConsume?: () => void;
  readonly onDrop?: () => void;
}

export interface LoopNotifyHandle {
  readonly dropped: boolean;
  drop(): void;
}

export interface IAgentLoopService {
  readonly _serviceBrand: undefined;

  submit(input: UserEntry, options?: LoopSubmitOptions): LoopSubmitResult;

  steer(promptIds: readonly string[]): Promise<void>;

  cancel(target?: LoopCancelTarget, reason?: unknown): boolean;

  snapshot(): LoopSnapshot;

  settled(): Promise<void>;

  tryAcquireQuiescence(): IDisposable | undefined;

  notify(note?: LoopNotify): LoopNotifyHandle;

  buildAttachBundle(): MachineEngineAttachBundle;

  attachEngine(ref: MachineEngineAttachRef, bundle: MachineEngineAttachBundle): MachineEngine;

  resetMachineEngine(): Promise<void>;

  promptHandle(id: string): PromptHandle | undefined;

  registerLoopErrorHandler(
    handler: LoopErrorHandler,
    options?: LoopErrorHandlerRegistrationOptions,
  ): IDisposable;

  readonly hooks: Hooks<{
    onWillBeginStep: BeforeStepContext;
    onDidFinishStep: AfterStepContext;
    onBeforeSubmitPrompt: PromptSubmitContext;
  }>;
}

export const IAgentLoopService = createDecorator<IAgentLoopService>('agentLoopService');