@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.
- package/package.json +8 -8
- package/src/adapter/cloudflare/agent.ts +160 -14
- package/src/adapter/cloudflare/define-agent.ts +12 -7
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +4 -4
- package/src/adapter/cloudflare/subagent/runner.ts +1 -1
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +29 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +9 -1
- package/src/config/model-guidance.ts +21 -0
- package/src/config/pinning.ts +22 -3
- package/src/config/ports.ts +27 -7
- package/src/config/public-contracts.ts +2 -2
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-stream.ts +2 -1
- package/src/conversation/history.ts +2 -0
- package/src/conversation/message/projection.ts +38 -21
- package/src/conversation/state.ts +23 -2
- package/src/harness/session-harness.ts +160 -6
- package/src/index.ts +8 -7
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/models/index.ts +32 -10
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +71 -11
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +12 -1
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +1 -1
- package/src/submission/repository.ts +64 -4
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +1 -1
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +317 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/{compiler.ts → runtime/compiler.ts} +84 -147
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1320 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /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
|
-
|
|
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
|
+
}
|
package/src/config/pinning.ts
CHANGED
|
@@ -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:
|
|
487
|
-
|
|
488
|
-
|
|
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,
|
package/src/config/ports.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
|
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
|
-
*
|
|
673
|
+
* Host 提供准备与提交操作;Runtime 在现有持久化审批中固定候选并等待用户确认。
|
|
673
674
|
*
|
|
674
|
-
*
|
|
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`
|
|
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
|
|
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
|
|
30
|
+
} from "../tools/subagent/authority";
|
package/src/context/assembly.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { estimateStringTokens } from "
|
|
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/
|
|
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
|
|
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:
|
|
168
|
-
hash
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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 =
|
|
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
|
|
191
|
+
state: changed
|
|
192
|
+
? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
|
|
193
|
+
: input.previousState,
|
|
173
194
|
changed,
|
|
174
195
|
};
|
|
175
196
|
}
|