@springbrand/agent-runtime 0.3.0-alpha.1 → 0.3.0-alpha.11

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.
Files changed (111) hide show
  1. package/package.json +8 -8
  2. package/src/adapter/cloudflare/agent.ts +160 -14
  3. package/src/adapter/cloudflare/define-agent.ts +12 -7
  4. package/src/adapter/cloudflare/index.ts +18 -0
  5. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  6. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  7. package/src/adapter/cloudflare/schema.ts +15 -0
  8. package/src/adapter/cloudflare/subagent/definition.ts +4 -4
  9. package/src/adapter/cloudflare/subagent/runner.ts +1 -1
  10. package/src/adapter/cloudflare/subagent/tools.ts +2 -2
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
  13. package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
  14. package/src/config/agent-context.ts +1 -1
  15. package/src/config/assembler.ts +29 -31
  16. package/src/config/assembly-snapshot.ts +1 -1
  17. package/src/config/definition.ts +1 -1
  18. package/src/config/harness-runtime.ts +9 -1
  19. package/src/config/model-guidance.ts +21 -0
  20. package/src/config/pinning.ts +22 -3
  21. package/src/config/ports.ts +27 -7
  22. package/src/config/public-contracts.ts +2 -2
  23. package/src/context/assembly.ts +1 -1
  24. package/src/context/extension/assembly.ts +2 -2
  25. package/src/context/memory/budget/gate.ts +19 -3
  26. package/src/conversation/harness-stream.ts +2 -1
  27. package/src/conversation/history.ts +2 -0
  28. package/src/conversation/message/projection.ts +38 -21
  29. package/src/conversation/state.ts +23 -2
  30. package/src/harness/session-harness.ts +160 -6
  31. package/src/index.ts +8 -7
  32. package/src/lib/artifacts.ts +18 -0
  33. package/src/lib/execution-level.ts +13 -0
  34. package/src/lib/prompt.ts +9 -10
  35. package/src/lib/token-estimate.ts +8 -0
  36. package/src/models/index.ts +32 -10
  37. package/src/models/openrouter/messages.ts +13 -8
  38. package/src/models/provider-streams.ts +45 -0
  39. package/src/session/cloudflare/migration.ts +71 -11
  40. package/src/skills/springbrand-worker-website/index.ts +1 -1
  41. package/src/submission/admission.ts +12 -1
  42. package/src/submission/contracts.ts +42 -2
  43. package/src/submission/delivery.ts +1 -1
  44. package/src/submission/finalization.ts +1 -1
  45. package/src/submission/repository.ts +64 -4
  46. package/src/submission/unattended.ts +40 -0
  47. package/src/telemetry/coordinator.ts +1 -1
  48. package/src/telemetry/subagent-lifecycle.ts +1 -1
  49. package/src/telemetry/turn-events.ts +1 -1
  50. package/src/tools/README.md +44 -0
  51. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  52. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  53. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  54. package/src/tools/assistant/ask-user.ts +317 -0
  55. package/src/tools/assistant/candidate.ts +15 -0
  56. package/src/tools/assistant/followups.ts +34 -0
  57. package/src/tools/assistant/index.ts +19 -0
  58. package/src/tools/assistant/memory.ts +73 -0
  59. package/src/tools/assistant/plan.ts +98 -0
  60. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  61. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  62. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  63. package/src/tools/codemode/workspace-connector.ts +198 -0
  64. package/src/tools/extension/index.ts +42 -0
  65. package/src/tools/index.ts +29 -26
  66. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  67. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  68. package/src/tools/runtime/activation.ts +40 -0
  69. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  70. package/src/tools/{compiler.ts → runtime/compiler.ts} +84 -147
  71. package/src/tools/runtime/contracts.ts +140 -0
  72. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
  73. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  74. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  75. package/src/tools/runtime/result.ts +22 -0
  76. package/src/tools/runtime/unattended-error.ts +4 -0
  77. package/src/tools/sandbox/index.ts +228 -0
  78. package/src/tools/schedule/index.ts +301 -0
  79. package/src/tools/skill/contracts.ts +20 -0
  80. package/src/tools/skill/index.ts +99 -0
  81. package/src/tools/skill/resources.ts +322 -0
  82. package/src/tools/skill/scripts.ts +168 -0
  83. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  84. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  85. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  86. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  87. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  88. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +1 -1
  89. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  90. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  91. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  92. package/src/tools/web/index.ts +2 -0
  93. package/src/tools/web/search/api.ts +1320 -0
  94. package/src/tools/web/search/index.ts +2 -0
  95. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  96. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  97. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  98. package/src/tools/base.ts +0 -534
  99. package/src/tools/schedule.ts +0 -261
  100. package/src/tools/skill.ts +0 -565
  101. package/src/tools/web-search/api.ts +0 -1318
  102. package/src/tools/web-search/index.ts +0 -2
  103. package/src/tools/workspace-state-connector.ts +0 -146
  104. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  105. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  106. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  107. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  108. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  109. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  110. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  111. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createModels,
3
+ lazyStream,
3
4
  type MutableModels,
4
5
  } from "@earendil-works/pi-ai";
5
6
  import {
@@ -23,6 +24,7 @@ import {
23
24
  type PreparePiRuntimeOptions,
24
25
  type PreparedPiRuntime,
25
26
  } from "./pinning";
27
+ import { refreshModelGuidance } from "./model-guidance";
26
28
 
27
29
  /**
28
30
  * Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
@@ -109,11 +111,17 @@ export class HarnessRuntimeConfig {
109
111
  : target.getModel(provider, modelId);
110
112
  }
111
113
  if (property === "streamSimple") {
112
- return withProviderBoundary(
114
+ const stream = withProviderBoundary(
113
115
  target.streamSimple.bind(target),
114
116
  request.modelSessionId,
115
117
  request.onGeneration,
116
118
  );
119
+ return ((model, context, options) => lazyStream(model, async () => {
120
+ options?.signal?.throwIfAborted();
121
+ const refreshed = await refreshModelGuidance(state.candidates, context);
122
+ options?.signal?.throwIfAborted();
123
+ return stream(model, refreshed, options);
124
+ })) satisfies typeof stream;
117
125
  }
118
126
  if (property === "completeSimple") {
119
127
  return target.completeSimple.bind(models);
@@ -0,0 +1,21 @@
1
+ import type { Context } from "@earendil-works/pi-ai";
2
+ import type { PiToolCandidate } from "../tools/runtime/contracts";
3
+
4
+ /** Only descriptions and guidance may vary inside a pinned Submission. */
5
+ export async function refreshModelGuidance(
6
+ candidates: readonly PiToolCandidate[],
7
+ context: Context,
8
+ ): Promise<Context> {
9
+ const guidance = new Map(await Promise.all(candidates
10
+ .filter(candidate => candidate.modelGuidance && context.tools?.some(tool => tool.name === candidate.tool.name))
11
+ .map(async candidate => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
12
+ if (!guidance.size) return context;
13
+ return {
14
+ ...context,
15
+ systemPrompt: [context.systemPrompt, ...new Set([...guidance.values()].map(item => item.systemPrompt).filter(Boolean))].filter(Boolean).join("\n\n"),
16
+ tools: context.tools?.map(tool => {
17
+ const prompt = guidance.get(tool.name);
18
+ return prompt ? { ...tool, description: prompt.description } : tool;
19
+ }),
20
+ };
21
+ }
@@ -157,6 +157,11 @@ export interface PreparedPiRuntime {
157
157
  export interface PinPiRuntimeOptions {
158
158
  readonly prepared: PreparedPiRuntime;
159
159
  readonly baseRevision: string;
160
+ /**
161
+ * Whether this Submission may inherit automatic hot and cold Memory context.
162
+ * Scheduled task input is intentionally isolated from those sources.
163
+ */
164
+ readonly includeMemory?: boolean;
160
165
  /**
161
166
  * 读取一个扩展要写入固定系统提示词的上下文。
162
167
  *
@@ -249,9 +254,20 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
249
254
  description: candidate.tool.description ?? candidate.tool.label,
250
255
  parameters: candidate.tool.parameters,
251
256
  requiredExecutionLevel: candidate.requiredExecutionLevel,
257
+ ...(candidate.confirmation ? { confirmation: true } : {}),
252
258
  ...(candidate.alwaysRequiresApproval
253
259
  ? { alwaysRequiresApproval: true }
254
260
  : {}),
261
+ ...(candidate.unattendedExecution
262
+ ? {
263
+ unattendedExecution: {
264
+ capability: candidate.unattendedExecution.capability,
265
+ operation: candidate.unattendedExecution.operation,
266
+ objectConstrained:
267
+ candidate.unattendedExecution.objectForInput !== undefined,
268
+ },
269
+ }
270
+ : {}),
255
271
  ...(candidate.deferLoading ? { deferLoading: true } : {}),
256
272
  ...(candidate.source ? { source: candidate.source } : {}),
257
273
  retry: piToolRetryPolicy(candidate),
@@ -481,11 +497,14 @@ export async function pinPiRuntime(
481
497
  ): Promise<PinnedPiRuntime> {
482
498
  const state = readPreparedPiRuntime(options.prepared, owner);
483
499
  const { snapshot, assembly } = state;
500
+ const includeMemory = options.includeMemory !== false;
484
501
  const context = await assemblePiSystemContext({
485
502
  baseSystemPrompt: snapshot.pi.systemPrompt,
486
- memory: snapshot.profile.memory,
487
- memoryPort: snapshot.bindings.memory,
488
- workspace: snapshot.bindings.workspace,
503
+ memory: includeMemory
504
+ ? snapshot.profile.memory
505
+ : { ...snapshot.profile.memory, enabled: false },
506
+ memoryPort: includeMemory ? snapshot.bindings.memory : undefined,
507
+ workspace: includeMemory ? snapshot.bindings.workspace : undefined,
489
508
  skills: snapshot.bindings.skills.sources,
490
509
  extensionContexts: assembly.context,
491
510
  readExtensionContext: options.readExtensionContext,
@@ -1,16 +1,17 @@
1
+ export type { RuntimeSchedulePreparation } from "../submission/contracts";
1
2
  import type { RuntimeModelInputModality } from "./profile";
2
3
  import type { SkillSource } from "agents/skills";
3
- import type { ScheduleSpec } from "../submission/contracts";
4
+ import type { ScheduleSpec, RuntimeSchedulePreparation } from "../submission/contracts";
4
5
  import type { RuntimeActivityProjection } from "../conversation/state";
5
6
  import type { ExecutionLevel } from "../lib/execution-level";
6
7
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
7
- import type { PiToolCandidate } from "../tools/compiler";
8
+ import type { PiToolCandidate } from "../tools/runtime/contracts";
8
9
  import type {
9
10
  RuntimeEventConfirmation,
10
11
  RuntimeLifecycleFact,
11
12
  RuntimeSubagentUsageEvent,
12
13
  RuntimeSubagentUsageMetrics,
13
- } from "../tools/subagent-authority";
14
+ } from "../tools/subagent/authority";
14
15
  export type { RuntimeModelInputModality } from "./profile";
15
16
  export type {
16
17
  RuntimeEventConfirmation,
@@ -21,7 +22,7 @@ export type {
21
22
  RuntimeSubmissionAdmissionHook,
22
23
  RuntimeSubmissionAdmissionInput,
23
24
  RuntimeSubmissionAdmissionResult,
24
- } from "../tools/subagent-authority";
25
+ } from "../tools/subagent/authority";
25
26
 
26
27
  // #region Workspace 端口
27
28
 
@@ -669,16 +670,25 @@ export interface RuntimeScheduleAgentSummary {
669
670
  * 让 Runtime 通过 Host 管理当前用户的定时任务。
670
671
  *
671
672
  * @remarks
672
- * Schedule Plugin 在装配时把它交给基础工具,模型在用户要求创建、查询或取消时调用。
673
+ * Host 提供准备与提交操作;Runtime 在现有持久化审批中固定候选并等待用户确认。
673
674
  *
674
- * 任务归属和持久化留在 Host,Runtime 只传递 `ScheduleSpec` 和窄结果。
675
+ * 任务事实、候选版本、授权检查和保存幂等由 Host 持有,不存放凭据。
675
676
  */
677
+ export interface RuntimeScheduleConfirmationPort {
678
+ /** Read-only preparation; bind the full candidate and authorization to this stable intent. */
679
+ prepare(request: RuntimeScheduleUpdate & { intentId: string; id?: string }): Promise<RuntimeSchedulePreparation>;
680
+ /** Atomically check candidate/version, live authorization and quotas. Replay must return the same task. */
681
+ commit(prepared: RuntimeSchedulePreparation, context: { intentId: string; signal: AbortSignal }): Promise<{ id: string }>;
682
+ }
683
+
676
684
  export interface RuntimeSchedulePort {
685
+ /** Required for chat creation/editing. Legacy management callers retain their CRUD contract. */
686
+ confirmation?: RuntimeScheduleConfirmationPort;
677
687
  /**
678
688
  * 创建一条定时任务并返回它的 ID。
679
689
  *
680
690
  * @remarks
681
- * `schedule` 工具在把用户的时间与提示词组成 `ScheduleSpec` 后调用。
691
+ * 保留给既有宿主管理调用者;聊天 `schedule` 只经 confirmation.commit 保存。
682
692
  *
683
693
  * Host 在该边界内绑定用户与会话,Runtime 不携带业务归属 ID。
684
694
  */
@@ -1020,6 +1030,16 @@ export interface RuntimePlatformPort {
1020
1030
  requiredExecutionLevel: ExecutionLevel;
1021
1031
  signal: AbortSignal;
1022
1032
  }): Promise<void>;
1033
+ /**
1034
+ * Match a pinned scheduled-task grant against the concrete Tool operation
1035
+ * and recheck the Host-owned live account authorization.
1036
+ *
1037
+ * This callback must fail closed by throwing a user-safe, specific reason.
1038
+ * It is called before dispatch and again at the effect boundary so a grant
1039
+ * revocation cannot be hidden by an earlier decision.
1040
+ */
1041
+ /** @deprecated Scheduled execution is authorized locally by execution level. */
1042
+ authorizeUnattendedExecution?(request: unknown): Promise<void>;
1023
1043
  }
1024
1044
 
1025
1045
  /**
@@ -17,7 +17,7 @@ export * from "../conversation/message";
17
17
  export * from "../submission/contracts";
18
18
  export * from "../conversation/state";
19
19
  export * from "../lib/execution-level";
20
- export type { WorkspaceRevision, WorkspaceRevisionManifest } from "../tools/workspace-versioning";
20
+ export type { WorkspaceRevision, WorkspaceRevisionManifest } from "../tools/workspace/versioning";
21
21
  export type {
22
22
  RuntimeEventConfirmation,
23
23
  RuntimeLifecycleFact,
@@ -27,4 +27,4 @@ export type {
27
27
  RuntimeSubmissionAdmissionHook,
28
28
  RuntimeSubmissionAdmissionInput,
29
29
  RuntimeSubmissionAdmissionResult,
30
- } from "../tools/subagent-authority";
30
+ } from "../tools/subagent/authority";
@@ -1,4 +1,4 @@
1
- import { estimateStringTokens } from "agents/experimental/memory/utils";
1
+ import { estimateStringTokens } from "../lib/token-estimate";
2
2
  import type { RuntimeMemoryPort } from "../config/ports";
3
3
  import type { RuntimeDegradation } from "../config/degradation";
4
4
  import type { RuntimeMemoryProfile } from "../config/profile";
@@ -12,8 +12,8 @@ import type {
12
12
  import type { RuntimeDegradation } from "../../config/degradation";
13
13
  import { withRuntimeLoadTimeout } from "../../config/runtime-load";
14
14
  import { sanitizeExtensionName } from "../../lib/extension-name";
15
- import type { PiToolCandidate } from "../../tools/compiler";
16
- import { createPiDeclaredToolCandidate } from "../../tools/declared";
15
+ import type { PiToolCandidate } from "../../tools/runtime/contracts";
16
+ import { createPiDeclaredToolCandidate } from "../../tools/adapters/declared";
17
17
 
18
18
  export interface PiExtensionContextContribution {
19
19
  readonly extensionName: string;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ artifactId,
2
3
  type ArtifactRef,
3
4
  serializeOutput,
4
5
  sha256Hex,
@@ -29,7 +30,7 @@ import {
29
30
  /**
30
31
  * 持久化单个字符串叶子的上限。
31
32
  *
32
- * MUST ≥ 单页 read 的上限(见 `workspace-sandbox.ts` 的 `READ_PAGE_MAX_CHARS`),
33
+ * MUST ≥ 单页 read 的上限(见 `tools/workspace/index.ts` 的 `READ_PAGE_MAX_CHARS`),
33
34
  * 否则持久记录会比模型当轮看到的内容还少,会话恢复后出现凭空缺页。
34
35
  */
35
36
  export const STORAGE_LEAF_MAX_CHARS = 64 * 1024;
@@ -161,11 +162,26 @@ export async function spillDurableToolOutput(
161
162
  text,
162
163
  ext === "json" ? "application/json" : "text/plain",
163
164
  );
165
+ if (!options.workspace.stat || !options.workspace.readFileBytes) {
166
+ throw new Error("Workspace artifact verification is unavailable");
167
+ }
168
+ const stat = await options.workspace.stat(path);
169
+ const expectedBytes = new TextEncoder().encode(text).byteLength;
170
+ if (!stat || stat.type !== "file" || stat.size !== expectedBytes) {
171
+ throw new Error("Workspace artifact failed size verification");
172
+ }
173
+ const stored = await options.workspace.readFileBytes(path);
174
+ if (!stored || new TextDecoder().decode(stored) !== text) {
175
+ throw new Error("Workspace artifact failed content verification");
176
+ }
164
177
  return {
165
178
  kind: "artifact_ref",
179
+ artifactId: artifactId(hash),
166
180
  path,
167
- bytes: new TextEncoder().encode(text).byteLength,
168
- hash: hash.slice(0, 16),
181
+ bytes: expectedBytes,
182
+ hash,
183
+ workspaceRevision: stat.contentVersion ?? `${stat.updatedAt}:${stat.size}`,
184
+ availability: "available",
169
185
  preview: text.slice(0, PREVIEW_CHARS),
170
186
  // 出口指令必须是可执行的:只说“去 read 这个路径”会诱导模型整读,而整读一个
171
187
  // 刚刚因为过大被外置的文件毫无意义。这里明确要求分页,并告知 read 会返回
@@ -2,13 +2,14 @@ import type { AgentEvent, Events, HarnessEvent } from "@earendil-works/pi-agent-
2
2
  import type { OperationResultRecord } from "@earendil-works/pi-agent-core/harness/session";
3
3
  import type { AssistantMessage } from "@earendil-works/pi-ai";
4
4
  import type { UIMessage, UIMessageChunk } from "ai";
5
- import { PiChunkEncoder } from "./message";
5
+ import { PiChunkEncoder, type ToolInputPreparer } from "./message";
6
6
 
7
7
  export interface HarnessStreamOptions {
8
8
  readonly messageId: string;
9
9
  readonly startedAt: number;
10
10
  readonly turnId: string;
11
11
  readonly userMessageId?: string | null;
12
+ readonly prepareToolInput?: ToolInputPreparer;
12
13
  }
13
14
 
14
15
  /** Project the final Host decision without rewriting Pi's canonical operation result. */
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@earendil-works/pi-agent-core";
6
6
  import type {
7
7
  AssistantMessage,
8
+ JsonValue,
8
9
  ToolResultMessage,
9
10
  UserMessage,
10
11
  } from "@earendil-works/pi-ai";
@@ -490,6 +491,7 @@ export class ConversationHistory {
490
491
  toolName: tool.toolName,
491
492
  ...toolResult,
492
493
  content: toolResult.content ?? [],
494
+ details: toolResult.details as JsonValue | undefined,
493
495
  isError: tool.status === "settled" && tool.isError,
494
496
  timestamp: snapshot.operation!.startedAt,
495
497
  }, { preliminary: tool.status === "running" });
@@ -75,6 +75,35 @@ function safeInput(value: unknown): Record<string, unknown> {
75
75
  return isRecord(safe) ? safe : {};
76
76
  }
77
77
 
78
+ export type ToolInputPreparer = (
79
+ toolName: string,
80
+ input: unknown,
81
+ ) => unknown;
82
+
83
+ function safeToolInput(
84
+ toolName: string,
85
+ value: unknown,
86
+ prepare?: ToolInputPreparer,
87
+ ): Record<string, unknown> {
88
+ if (prepare) {
89
+ try {
90
+ value = prepare(toolName, value);
91
+ } catch {
92
+ // Preserve invalid provider output for the normal Tool validation path.
93
+ }
94
+ }
95
+ const input = safeInput(value);
96
+ if (toolName === "ask_user" && typeof input.questions === "string") {
97
+ try {
98
+ const questions: unknown = JSON.parse(input.questions);
99
+ if (Array.isArray(questions)) input.questions = questions;
100
+ } catch {
101
+ // Preserve malformed provider output for the normal unavailable state.
102
+ }
103
+ }
104
+ return input;
105
+ }
106
+
78
107
  function contentText(result: unknown): string | undefined {
79
108
  if (!isRecord(result) || !Array.isArray(result.content)) return undefined;
80
109
  const text = result.content.flatMap((part) =>
@@ -175,6 +204,7 @@ export class PiChunkEncoder {
175
204
  private readonly startedAt?: number;
176
205
  private readonly turnId?: string;
177
206
  private readonly userMessageId?: string;
207
+ private readonly prepareToolInput?: ToolInputPreparer;
178
208
  private started = false;
179
209
  private finished = false;
180
210
  private firstTurnStarted = false;
@@ -194,11 +224,13 @@ export class PiChunkEncoder {
194
224
  startedAt?: number;
195
225
  turnId?: string;
196
226
  userMessageId?: string | null;
227
+ prepareToolInput?: ToolInputPreparer;
197
228
  }) {
198
229
  this.messageId = options.messageId;
199
230
  this.startedAt = options.startedAt;
200
231
  this.turnId = options.turnId;
201
232
  this.userMessageId = options.userMessageId ?? undefined;
233
+ this.prepareToolInput = options.prepareToolInput;
202
234
  }
203
235
 
204
236
  start(): UIMessageChunk[] {
@@ -277,7 +309,7 @@ export class PiChunkEncoder {
277
309
  toolName: string,
278
310
  input: unknown,
279
311
  ): UIMessageChunk[] {
280
- const safe = safeInput(input);
312
+ const safe = safeToolInput(toolName, input, this.prepareToolInput);
281
313
  this.toolInputs.set(toolCallId, { toolName, input: safe });
282
314
  return [{
283
315
  type: "tool-input-available",
@@ -505,18 +537,11 @@ export class PiChunkEncoder {
505
537
  streaming.deltas.length = 0;
506
538
  }
507
539
  if (update.type === "toolcall_end") {
508
- const input = safeInput(update.toolCall.arguments);
509
- this.toolInputs.set(toolCallId, {
510
- toolName: update.toolCall.name,
511
- input,
512
- });
513
- chunks.push({
514
- type: "tool-input-available",
540
+ chunks.push(...this.toolInput(
515
541
  toolCallId,
516
- toolName: update.toolCall.name,
517
- input,
518
- dynamic: true,
519
- });
542
+ update.toolCall.name,
543
+ update.toolCall.arguments,
544
+ ));
520
545
  this.pendingToolByIndex.delete(index);
521
546
  this.toolCallIdByIndex.delete(index);
522
547
  }
@@ -613,15 +638,7 @@ function projectedTool(
613
638
  call: ToolCall,
614
639
  approval?: PiToolApprovalView,
615
640
  ): DynamicToolUIPart {
616
- const input = safeInput(call.arguments);
617
- if (call.name === "ask_user" && typeof input.questions === "string") {
618
- try {
619
- const questions: unknown = JSON.parse(input.questions);
620
- if (Array.isArray(questions)) input.questions = questions;
621
- } catch {
622
- // Preserve malformed provider output for the normal unavailable state.
623
- }
624
- }
641
+ const input = safeToolInput(call.name, call.arguments);
625
642
  const base = {
626
643
  type: "dynamic-tool" as const,
627
644
  toolCallId: call.id,
@@ -1,6 +1,6 @@
1
1
  import type { ApprovalReceipt } from "../submission/contracts";
2
2
  import type { StoredSubmission } from "../submission/repository";
3
- import type { PendingApprovalSummary } from "../tools/approval/repository";
3
+ import type { PendingApprovalSummary } from "../tools/runtime/approval/repository";
4
4
  import type { PiCanonicalUserInput } from "./message/contract";
5
5
 
6
6
  export type RuntimeLoadPhase = "config" | "assembly" | "mcp" | "pi";
@@ -68,6 +68,17 @@ export interface RuntimeTurnState {
68
68
  queued: RuntimeQueuedSubmission[];
69
69
  }
70
70
 
71
+ /**
72
+ * 本 Session 当前是否拒绝普通用户输入。
73
+ *
74
+ * 定时 Run 未结束时它独占输入:宿主据此禁用输入框并显示原因。
75
+ * 服务端准入用同一事实拒绝消息,所以客户端状态与后端判定不会漂移。
76
+ */
77
+ export interface RuntimeInputLock {
78
+ /** 稳定原因码,由宿主/UI 决定文案。 */
79
+ reason: string;
80
+ }
81
+
71
82
  /**
72
83
  * 保存 Kernel 在当前会话里自己维护的审批状态。
73
84
  *
@@ -85,6 +96,8 @@ export interface RuntimeState {
85
96
  activity?: RuntimeActivityProjection;
86
97
  approvals?: ApprovalReceipt[];
87
98
  turn?: RuntimeTurnState;
99
+ /** 有值时普通输入被锁定,直到锁定的 Submission 进入终态。 */
100
+ inputLock?: RuntimeInputLock;
88
101
  }
89
102
 
90
103
  type SubmissionProjection = Pick<
@@ -103,6 +116,8 @@ export function projectRuntimeState(input: {
103
116
  readonly hasPendingInteraction: (submissionId: string) => boolean;
104
117
  readonly hasPendingSteer: (submissionId: string) => boolean;
105
118
  readonly hasRunningAgentTools: boolean;
119
+ /** A non-terminal scheduled Submission owns ordinary input until it ends. */
120
+ readonly hasUnfinishedScheduled?: boolean;
106
121
  readonly previousState: RuntimeState;
107
122
  }): { state: RuntimeState; changed: boolean } {
108
123
  const approvals: ApprovalReceipt[] = [
@@ -164,12 +179,18 @@ export function projectRuntimeState(input: {
164
179
  backgroundWork: input.hasRunningAgentTools,
165
180
  revision: (previousActivity?.revision ?? 0) + 1,
166
181
  };
182
+ const inputLock: RuntimeInputLock | undefined = input.hasUnfinishedScheduled
183
+ ? { reason: "scheduled-run" }
184
+ : undefined;
167
185
  const changed = input.previousState.approvals === undefined ||
168
186
  JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
169
187
  JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
188
+ JSON.stringify(input.previousState.inputLock) !== JSON.stringify(inputLock) ||
170
189
  nextActivity !== previousActivity;
171
190
  return {
172
- state: changed ? { ...input.previousState, activity: nextActivity, approvals, turn } : input.previousState,
191
+ state: changed
192
+ ? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
193
+ : input.previousState,
173
194
  changed,
174
195
  };
175
196
  }