@springbrand/agent-runtime 0.2.0-alpha.44 → 0.2.0-alpha.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ import {
58
58
  codeExecutionPiToolCandidate,
59
59
  } from "./pi/tool/core";
60
60
  import { skillPiToolCandidates } from "./pi/tool/skill";
61
- import { createWebSearch } from "./pi/tool/web-search";
61
+ import { createWebSearch, WEB_SEARCH_MODEL } from "./pi/tool/web-search";
62
62
  import { subagentPiToolCandidates } from "./pi/tool/subagent";
63
63
  import { resolvePiModel } from "./pi/runtime-adapter/models";
64
64
  import {
@@ -170,7 +170,7 @@ interface ToolSurfaceInput {
170
170
  readonly hostTools: readonly PiToolCandidate[];
171
171
  readonly skills: readonly RuntimeSkillSourceBinding[];
172
172
  readonly enabledSubagents: readonly string[];
173
- readonly webSearch: Parameters<typeof basePiToolCandidates>[0];
173
+ readonly webSearch?: Parameters<typeof basePiToolCandidates>[0];
174
174
  readonly extensions: readonly RuntimeExtensionConfig[];
175
175
  readonly policy?: RuntimeToolSurfacePolicy;
176
176
  }
@@ -581,14 +581,20 @@ class RuntimeBuilder {
581
581
  : `Model is configured by multiple endpoints: ${profile.model}`,
582
582
  );
583
583
  }
584
- const resolvedModel = resolvePiModel(provider, profile.model);
585
- const endpoint = endpoints[0]!;
586
- const webSearch = createWebSearch({
587
- endpoint,
588
- model: profile.model,
589
- maxTokens: resolvedModel.maxTokens,
590
- reasoning: resolvedModel.reasoning,
591
- });
584
+ const webSearchEndpoint = provider.endpoints.find((endpoint) =>
585
+ endpoint.models.includes(WEB_SEARCH_MODEL)
586
+ );
587
+ const webSearch = webSearchEndpoint
588
+ ? (() => {
589
+ const webSearchModel = resolvePiModel(provider, WEB_SEARCH_MODEL);
590
+ return createWebSearch({
591
+ endpoint: webSearchEndpoint,
592
+ model: WEB_SEARCH_MODEL,
593
+ maxTokens: webSearchModel.maxTokens,
594
+ reasoning: webSearchModel.reasoning,
595
+ });
596
+ })()
597
+ : undefined;
592
598
  const skillSources = [...this.skillSources.values()];
593
599
  const toolSurface = await createToolSurface({
594
600
  ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
@@ -606,7 +612,7 @@ class RuntimeBuilder {
606
612
  ],
607
613
  skills: skillSources,
608
614
  enabledSubagents: [...this.enabledSubagents],
609
- webSearch,
615
+ ...(webSearch ? { webSearch } : {}),
610
616
  extensions: [...this.extensions.values()],
611
617
  ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
612
618
  });
package/src/runtime.ts CHANGED
@@ -48,7 +48,6 @@ import type {
48
48
  } from "./kernel/extensions";
49
49
  import type {
50
50
  RuntimeActivity,
51
- RuntimeActivityProjection,
52
51
  RuntimeState,
53
52
  RuntimeTurnState,
54
53
  } from "./kernel/state";
@@ -67,6 +66,7 @@ import {
67
66
  } from "./telemetry";
68
67
  import { connectConfiguredMcpServers } from "./lib/mcp";
69
68
  import {
69
+ ContextOverflowError,
70
70
  PiRuntimeAdapter,
71
71
  MODEL_STREAM_STALL_TIMEOUT_MS,
72
72
  readModelStreamStallDetails,
@@ -110,6 +110,7 @@ import {
110
110
  isTerminalSubmissionStatus,
111
111
  RuntimeDatabase,
112
112
  type StoredSubmissionAdmission,
113
+ type SubmissionRecoveryReason,
113
114
  type SubmissionStatus,
114
115
  } from "./db/index";
115
116
  import {
@@ -151,10 +152,11 @@ export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
151
152
  export const CHAT_STALL_MAX_ATTEMPTS = 3;
152
153
  const CHAT_RECOVERY_TERMINAL_MESSAGE =
153
154
  "Recovery failed after multiple attempts. This generation has stopped and your progress has been saved. Send a new message to continue.";
155
+ const CONTEXT_OVERFLOW_TERMINAL_MESSAGE =
156
+ "The model context is still too large after forced compaction. Start a new conversation or reduce the attached context.";
154
157
 
155
158
  type RuntimeEventOutboxPayload =
156
159
  | { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
157
- | { readonly type: "activity"; readonly projection: RuntimeActivityProjection }
158
160
  | {
159
161
  readonly type: "tool-settlement";
160
162
  readonly event: RuntimeToolSettlementEvent;
@@ -174,7 +176,10 @@ interface StoredSubmission extends SubmissionReceipt {
174
176
  userMessageId?: string | null;
175
177
  regenerateMessageId?: string | null;
176
178
  recoveryErrorCount: number;
177
- recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
179
+ recoveryReason?:
180
+ | "no_meaningful_model_progress"
181
+ | "transient_model_error"
182
+ | "context_overflow";
178
183
  runId: string | null;
179
184
  accountId: string | null;
180
185
  rateVersion: number | null;
@@ -534,6 +539,8 @@ export abstract class AgentRuntimeKernel<
534
539
  this.db.submissions.findRunning() as StoredSubmission | null,
535
540
  findNextPending: () =>
536
541
  this.db.submissions.findNextPending() as StoredSubmission | null,
542
+ listPending: () =>
543
+ this.db.submissions.listPending() as StoredSubmission[],
537
544
  updateAbortReason: (submissionId, reason) =>
538
545
  this.db.submissions.updateAbortReason(submissionId, reason),
539
546
  };
@@ -1001,6 +1008,26 @@ export abstract class AgentRuntimeKernel<
1001
1008
  });
1002
1009
  this.projectNestedToolSettlement(submission.submissionId, event);
1003
1010
  },
1011
+ onUnknownToolStarted: (event) => {
1012
+ this.telemetry.capture("toolStarted", {
1013
+ submissionId: submission.submissionId,
1014
+ toolCallId: event.toolCallId,
1015
+ toolName: event.toolName,
1016
+ input: event.input,
1017
+ occurredAt: event.occurredAt,
1018
+ });
1019
+ },
1020
+ onUnknownToolFinished: (event) => {
1021
+ this.telemetry.capture("toolFinished", {
1022
+ submissionId: submission.submissionId,
1023
+ toolCallId: event.toolCallId,
1024
+ toolName: event.toolName,
1025
+ outcome: event.outcome,
1026
+ output: event.output,
1027
+ outputBytes: json(event.output).length,
1028
+ occurredAt: event.occurredAt,
1029
+ });
1030
+ },
1004
1031
  ...(toolExecutors && Object.keys(toolExecutors).length > 0
1005
1032
  ? { toolExecutors }
1006
1033
  : {}),
@@ -1107,12 +1134,12 @@ export abstract class AgentRuntimeKernel<
1107
1134
  async onStart(): Promise<void> {
1108
1135
  this.runtimeLoad.reset();
1109
1136
  this.telemetry.recover(() => this.ensureRuntimeReady());
1110
- await this.broadcastApprovals();
1111
1137
  if (this.db.runtimeEvents.hasPending()) {
1112
1138
  this.ctx.waitUntil(
1113
1139
  this.ensureRuntimeReady().then(() => this.drainRuntimeEvents()),
1114
1140
  );
1115
1141
  }
1142
+ await this.broadcastApprovals();
1116
1143
  await this.approvals.dispatchPendingContinuations();
1117
1144
  const running = this.db.submissions.findRunning() as StoredSubmission | null;
1118
1145
  if (running) {
@@ -1136,13 +1163,6 @@ export abstract class AgentRuntimeKernel<
1136
1163
  const payload = JSON.parse(row.body) as RuntimeEventOutboxPayload;
1137
1164
  if (payload.type === "model-usage") {
1138
1165
  await turnEvents.onModelUsage?.(payload.event);
1139
- } else if (payload.type === "activity") {
1140
- if (
1141
- !this.state.activity ||
1142
- payload.projection.revision >= this.state.activity.revision
1143
- ) {
1144
- await turnEvents.onActivityChanged?.(payload.projection);
1145
- }
1146
1166
  } else if (payload.type === "tool-settlement") {
1147
1167
  await turnEvents.onToolSettled?.(payload.event);
1148
1168
  } else if (payload.type === "subagent-usage") {
@@ -1671,7 +1691,7 @@ export abstract class AgentRuntimeKernel<
1671
1691
 
1672
1692
  // 作用:按当前模型输入预算压缩 Pi 上下文。
1673
1693
  // 调用:Pi Turn 适配器在发模型请求前通过 `transformContext` 回调。
1674
- // 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型和密钥。
1694
+ // 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型。
1675
1695
  private async transformPiContext(
1676
1696
  submissionId: string,
1677
1697
  messages: Parameters<PiRuntimeTranscript["compactContext"]>[0],
@@ -1680,13 +1700,12 @@ export abstract class AgentRuntimeKernel<
1680
1700
  const snapshot = this.assembly();
1681
1701
  const inputBudget = snapshot.pi.model.contextWindow -
1682
1702
  snapshot.pi.model.maxTokens;
1703
+ const force = this.readSubmission(submissionId)?.recoveryReason ===
1704
+ "context_overflow";
1683
1705
  const compacted = await this.transcript.compactContext(messages, {
1684
1706
  compactAfterTokens: inputBudget * 0.85,
1707
+ force,
1685
1708
  model: snapshot.pi.model,
1686
- apiKey: this.pi.resolveApiKey(
1687
- snapshot.bindings.provider,
1688
- snapshot.pi.model.id,
1689
- ),
1690
1709
  signal,
1691
1710
  submissionId,
1692
1711
  onCompactionPersisted: (event) => {
@@ -1719,6 +1738,7 @@ export abstract class AgentRuntimeKernel<
1719
1738
  });
1720
1739
  },
1721
1740
  });
1741
+ if (force) this.db.submissions.clearRecoveryReason(submissionId);
1722
1742
  await this.drainRuntimeEvents();
1723
1743
  return compacted;
1724
1744
  }
@@ -2849,7 +2869,9 @@ export abstract class AgentRuntimeKernel<
2849
2869
  reason: submission.recoveryReason ?? "runtime_restart",
2850
2870
  attempt: Math.max(1, submission.recoveryErrorCount),
2851
2871
  });
2852
- this.db.submissions.clearRecoveryReason(submissionId);
2872
+ if (submission.recoveryReason !== "context_overflow") {
2873
+ this.db.submissions.clearRecoveryReason(submissionId);
2874
+ }
2853
2875
  await this.broadcastApprovals();
2854
2876
  }
2855
2877
  if (mode !== "fresh") await this.ensureRuntimeReady();
@@ -3144,15 +3166,25 @@ export abstract class AgentRuntimeKernel<
3144
3166
  return this.readSubmission(submissionId)!;
3145
3167
  }
3146
3168
  const stalled = error instanceof ChatStreamStalledError;
3169
+ const contextOverflow = error instanceof ContextOverflowError;
3147
3170
  const abortReason = this.readSubmission(submissionId)?.abortReason;
3148
3171
  if (!abortReason && (stalled || error instanceof RetryableModelError)) {
3149
- const recoveryErrorCount = this.db.submissions
3150
- .incrementRecoveryErrorCount(
3172
+ const recoveryReason: SubmissionRecoveryReason = stalled
3173
+ ? "no_meaningful_model_progress"
3174
+ : contextOverflow
3175
+ ? "context_overflow"
3176
+ : "transient_model_error";
3177
+ let contextOverflowRecoveryClaimed = false;
3178
+ const recoveryErrorCount = this.db.transaction(() => {
3179
+ if (contextOverflow) {
3180
+ contextOverflowRecoveryClaimed = this.db.submissions
3181
+ .claimContextOverflowRecovery(submissionId);
3182
+ }
3183
+ return this.db.submissions.incrementRecoveryErrorCount(
3151
3184
  submissionId,
3152
- stalled
3153
- ? "no_meaningful_model_progress"
3154
- : "transient_model_error",
3185
+ recoveryReason,
3155
3186
  );
3187
+ });
3156
3188
  if (stalled) {
3157
3189
  const stallDetails = readModelStreamStallDetails(errorText(error)) ?? {
3158
3190
  lastMeaningfulActivityAt:
@@ -3172,10 +3204,14 @@ export abstract class AgentRuntimeKernel<
3172
3204
  await this.broadcastApprovals();
3173
3205
  const stoppedDuringRecovery = this.readSubmission(submissionId)
3174
3206
  ?.abortReason;
3207
+ const retryAllowed = contextOverflow
3208
+ ? contextOverflowRecoveryClaimed &&
3209
+ recoveryErrorCount < CHAT_RECOVERY_MAX_ATTEMPTS
3210
+ : recoveryErrorCount <
3211
+ (stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS);
3175
3212
  if (
3176
3213
  !stoppedDuringRecovery &&
3177
- recoveryErrorCount <
3178
- (stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS)
3214
+ retryAllowed
3179
3215
  ) {
3180
3216
  const recoveryOutcome = await this.scheduleChatRecoveryRetry(
3181
3217
  {
@@ -3216,7 +3252,9 @@ export abstract class AgentRuntimeKernel<
3216
3252
  } else if (!stoppedDuringRecovery) {
3217
3253
  terminalIntent = {
3218
3254
  outcome: "failed",
3219
- message: CHAT_RECOVERY_TERMINAL_MESSAGE,
3255
+ message: contextOverflow
3256
+ ? CONTEXT_OVERFLOW_TERMINAL_MESSAGE
3257
+ : CHAT_RECOVERY_TERMINAL_MESSAGE,
3220
3258
  };
3221
3259
  }
3222
3260
  }
@@ -4114,6 +4152,35 @@ export abstract class AgentRuntimeKernel<
4114
4152
  return result;
4115
4153
  }
4116
4154
 
4155
+ /** Stops every running, queued, or steered user request in this Session. */
4156
+ async stopAllSubmissions(reason?: string): Promise<{ ok: boolean }> {
4157
+ const current = this.db.submissions.findRunning() ??
4158
+ this.db.submissions.findNextPending();
4159
+ if (current) {
4160
+ this.db.transaction(() => {
4161
+ for (const steer of this.db.steers.listForSubmission(
4162
+ current.submissionId,
4163
+ )) {
4164
+ const message = JSON.parse(
4165
+ steer.canonicalJson,
4166
+ ) as PiCanonicalUserInput;
4167
+ const userMessage = steer.uiMessageJson
4168
+ ? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
4169
+ : undefined;
4170
+ this.transcript.append(steer.messageId, message, {
4171
+ submissionId: current.submissionId,
4172
+ createdAt: steer.createdAt,
4173
+ ...(userMessage ? { userMessage } : {}),
4174
+ });
4175
+ this.db.steers.deleteByMessageId(steer.messageId);
4176
+ }
4177
+ });
4178
+ }
4179
+ const result = await this.submissions.stopAll(reason ?? "Stopped");
4180
+ await this.broadcastApprovals();
4181
+ return result;
4182
+ }
4183
+
4117
4184
  /** 返回当前已装配 User Agent 的执行档位,不提供 Session 覆盖。 */
4118
4185
  executionLevel(): ExecutionLevel {
4119
4186
  return this.assembly().profile.executionLevel;
@@ -4562,13 +4629,20 @@ export abstract class AgentRuntimeKernel<
4562
4629
  ...(current.recoveryErrorCount === undefined
4563
4630
  ? {}
4564
4631
  : {
4565
- recoveryAttempt: current.recoveryErrorCount,
4632
+ recoveryAttempt:
4633
+ current.recoveryReason === "context_overflow"
4634
+ ? 1
4635
+ : current.recoveryErrorCount,
4566
4636
  // 上限按当前恢复原因取,否则 stall 会显示 "2/5" 却在第 3 次就终止。
4567
4637
  recoveryMax:
4568
- current.recoveryReason === "no_meaningful_model_progress"
4569
- ? CHAT_STALL_MAX_ATTEMPTS
4570
- : CHAT_RECOVERY_MAX_ATTEMPTS,
4571
- ...(current.recoveryReason
4638
+ current.recoveryReason === "context_overflow"
4639
+ ? 1
4640
+ : current.recoveryReason ===
4641
+ "no_meaningful_model_progress"
4642
+ ? CHAT_STALL_MAX_ATTEMPTS
4643
+ : CHAT_RECOVERY_MAX_ATTEMPTS,
4644
+ ...(current.recoveryReason &&
4645
+ current.recoveryReason !== "context_overflow"
4572
4646
  ? { recoveryReason: current.recoveryReason }
4573
4647
  : {}),
4574
4648
  }),
@@ -4607,13 +4681,6 @@ export abstract class AgentRuntimeKernel<
4607
4681
  backgroundWork,
4608
4682
  revision: (currentActivity?.revision ?? 0) + 1,
4609
4683
  };
4610
- this.db.transaction(() => {
4611
- this.db.runtimeEvents.insert({
4612
- eventId: `session-activity:${nextActivity.revision}`,
4613
- body: json({ type: "activity", projection: nextActivity }),
4614
- createdAt: Date.now(),
4615
- });
4616
- });
4617
4684
  if (
4618
4685
  this.state?.approvals === undefined ||
4619
4686
  json(this.state.approvals) !== json(approvals) ||
@@ -4627,9 +4694,14 @@ export abstract class AgentRuntimeKernel<
4627
4694
  turn,
4628
4695
  });
4629
4696
  }
4630
- this.ctx.waitUntil(this.drainRuntimeEvents());
4697
+ const projection = this.turnEventsPort()?.onActivityChanged?.(nextActivity);
4698
+ if (projection) {
4699
+ this.ctx.waitUntil(
4700
+ projection.catch(() => undefined),
4701
+ );
4702
+ }
4631
4703
  } catch {
4632
- // Rebuilding UI state is best effort; queued activity delivery retries independently.
4704
+ // Approval projection is best effort and must not block execution.
4633
4705
  }
4634
4706
  }
4635
4707