@springbrand/agent-runtime 0.3.0-alpha.5 → 0.3.0-alpha.7
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 +1 -1
- package/src/adapter/cloudflare/agent.ts +9 -3
- package/src/config/harness-runtime.ts +9 -1
- package/src/config/model-guidance.ts +21 -0
- package/src/config/pinning.ts +11 -3
- package/src/harness/session-harness.ts +3 -2
- package/src/tools/compiler.ts +2 -0
- package/src/tools/schedule.ts +17 -10
package/package.json
CHANGED
|
@@ -519,11 +519,15 @@ export abstract class AgentRuntimeKernel<
|
|
|
519
519
|
}> {
|
|
520
520
|
if (fresh) await this.prepareRuntimeForSubmission(submissionId);
|
|
521
521
|
else await this.ensureRuntimeReady();
|
|
522
|
-
|
|
522
|
+
const submission = this.db.submissions.find(submissionId);
|
|
523
|
+
if (!submission) {
|
|
524
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
525
|
+
}
|
|
526
|
+
return this.currentAssemblyPin(submission.origin !== "scheduled");
|
|
523
527
|
}
|
|
524
528
|
|
|
525
529
|
// initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
|
|
526
|
-
private async currentAssemblyPin(): Promise<{
|
|
530
|
+
private async currentAssemblyPin(includeMemory = true): Promise<{
|
|
527
531
|
revision: string;
|
|
528
532
|
descriptor: string;
|
|
529
533
|
}> {
|
|
@@ -532,16 +536,18 @@ export abstract class AgentRuntimeKernel<
|
|
|
532
536
|
if (!runtime) {
|
|
533
537
|
throw new Error("Runtime revision is not initialized");
|
|
534
538
|
}
|
|
535
|
-
return this.pinAssembly(runtime.pi, runtime.revision);
|
|
539
|
+
return this.pinAssembly(runtime.pi, runtime.revision, includeMemory);
|
|
536
540
|
}
|
|
537
541
|
|
|
538
542
|
private async pinAssembly(
|
|
539
543
|
prepared: PreparedPiRuntime,
|
|
540
544
|
baseRevision: string,
|
|
545
|
+
includeMemory: boolean,
|
|
541
546
|
): Promise<{ revision: string; descriptor: string }> {
|
|
542
547
|
const pinned = await this.pi.pin({
|
|
543
548
|
prepared,
|
|
544
549
|
baseRevision,
|
|
550
|
+
includeMemory,
|
|
545
551
|
readExtensionContext: ({ label }) =>
|
|
546
552
|
this._hostGetContext(label),
|
|
547
553
|
});
|
|
@@ -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/compiler";
|
|
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
|
*
|
|
@@ -492,11 +497,14 @@ export async function pinPiRuntime(
|
|
|
492
497
|
): Promise<PinnedPiRuntime> {
|
|
493
498
|
const state = readPreparedPiRuntime(options.prepared, owner);
|
|
494
499
|
const { snapshot, assembly } = state;
|
|
500
|
+
const includeMemory = options.includeMemory !== false;
|
|
495
501
|
const context = await assemblePiSystemContext({
|
|
496
502
|
baseSystemPrompt: snapshot.pi.systemPrompt,
|
|
497
|
-
memory:
|
|
498
|
-
|
|
499
|
-
|
|
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,
|
|
500
508
|
skills: snapshot.bindings.skills.sources,
|
|
501
509
|
extensionContexts: assembly.context,
|
|
502
510
|
readExtensionContext: options.readExtensionContext,
|
|
@@ -175,12 +175,13 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
private async operationEntries(context: Context): Promise<Entry[]> {
|
|
178
|
+
// Stop boundaries follow traversal order; walk back from the current tip.
|
|
178
179
|
const entries = await this.lane.findEntries({
|
|
179
180
|
...(this.operationStartTip === null ? {} : { stopAtId: this.operationStartTip }),
|
|
180
181
|
type: "message",
|
|
181
|
-
order: "
|
|
182
|
+
order: "newestFirst",
|
|
182
183
|
}, context);
|
|
183
|
-
return entries.filter(({ id }) => id !== this.operationStartTip);
|
|
184
|
+
return entries.filter(({ id }) => id !== this.operationStartTip).reverse();
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
private async restoreToolGovernance(context: Context): Promise<void> {
|
package/src/tools/compiler.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface PiToolConfirmationSpec {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface PiToolCandidate {
|
|
65
|
+
/** Presentation only, resolved at the model boundary; never changes approved execution inputs. */
|
|
66
|
+
readonly modelGuidance?: () => Promise<{ description: string; systemPrompt?: string }>;
|
|
65
67
|
readonly confirmation?: PiToolConfirmationSpec;
|
|
66
68
|
readonly owner: string;
|
|
67
69
|
readonly tool: AgentTool<any, any>;
|
package/src/tools/schedule.ts
CHANGED
|
@@ -25,34 +25,41 @@ const scheduleTriggerParameters = Type.Union([
|
|
|
25
25
|
endDate: Type.Optional(Type.String({ description: "Inclusive last local date YYYY-MM-DD." })),
|
|
26
26
|
}, { additionalProperties: false }),
|
|
27
27
|
Type.Object({
|
|
28
|
-
kind: Type.Literal("once-in"),
|
|
28
|
+
kind: Type.Literal("once-in", { description: "Run once after a delay." }),
|
|
29
29
|
seconds: Type.Integer({
|
|
30
30
|
minimum: 1,
|
|
31
31
|
description: "Fire once, this many seconds from now.",
|
|
32
32
|
}),
|
|
33
33
|
}, { additionalProperties: false }),
|
|
34
34
|
Type.Object({
|
|
35
|
-
kind: Type.Literal("once-at"),
|
|
35
|
+
kind: Type.Literal("once-at", { description: "Run once at an absolute UTC time." }),
|
|
36
36
|
at: Type.String({
|
|
37
37
|
description:
|
|
38
38
|
"Fire once at this absolute UTC ISO time, e.g. 2026-07-20T09:00:00Z.",
|
|
39
39
|
}),
|
|
40
40
|
}, { additionalProperties: false }),
|
|
41
41
|
Type.Object({
|
|
42
|
-
kind: Type.Literal("cron"),
|
|
42
|
+
kind: Type.Literal("cron", { description: "Repeat on a UTC cron schedule." }),
|
|
43
43
|
cron: Type.String({
|
|
44
44
|
description:
|
|
45
45
|
"Repeat on a standard UTC cron, e.g. '0 9 * * *' = daily 09:00 UTC.",
|
|
46
46
|
}),
|
|
47
47
|
}, { additionalProperties: false }),
|
|
48
48
|
Type.Object({
|
|
49
|
-
kind: Type.Literal("interval"),
|
|
49
|
+
kind: Type.Literal("interval", { description: "Repeat at fixed intervals." }),
|
|
50
50
|
seconds: Type.Integer({
|
|
51
51
|
minimum: 1,
|
|
52
52
|
description: "Repeat every this many seconds.",
|
|
53
53
|
}),
|
|
54
54
|
}, { additionalProperties: false }),
|
|
55
|
-
], {
|
|
55
|
+
], {
|
|
56
|
+
description:
|
|
57
|
+
"When to fire. Discriminate on the `kind` field (never \"type\"). " +
|
|
58
|
+
'Shapes: {"kind":"local","rule":{"kind":"once","date":"2026-09-17","time":"14:06"}} runs once at 14:06 in `timezone`; ' +
|
|
59
|
+
'{"kind":"local","rule":{"kind":"daily","time":"09:00"}} runs daily; ' +
|
|
60
|
+
'{"kind":"local","rule":{"kind":"weekly","weekdays":[1,3],"time":"09:30"}} runs on Mondays and Wednesdays; ' +
|
|
61
|
+
'{"kind":"once-at","at":"2026-09-17T06:06:00Z"} fires once at an absolute UTC time.',
|
|
62
|
+
});
|
|
56
63
|
|
|
57
64
|
const scheduleParameters = Type.Object({
|
|
58
65
|
prompt: Type.String({ minLength: 1, maxLength: 1000, pattern: "\\S",
|
|
@@ -125,7 +132,7 @@ export function schedulePiToolCandidates(
|
|
|
125
132
|
name: "schedule",
|
|
126
133
|
label: "Schedule prompt",
|
|
127
134
|
description:
|
|
128
|
-
"Schedule a prompt to run LATER, autonomously. Required inputs: prompt (Requirement, at most 1000 Unicode characters), trigger and timezone. Prefer local once/daily/weekly rules. label is optional (at most 50 characters); the Host generates an omitted name. Ask for missing information. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
|
|
135
|
+
"Schedule a prompt to run LATER, autonomously. Required inputs: prompt (Requirement, at most 1000 Unicode characters), trigger and timezone. Prefer local once/daily/weekly rules. trigger discriminates on `kind` (never \"type\"); see the trigger parameter schema for detailed shapes. label is optional (at most 50 characters); the Host generates an omitted name. Ask for missing information. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
|
|
129
136
|
parameters: scheduleParameters,
|
|
130
137
|
async execute(_toolCallId, input, signal) {
|
|
131
138
|
signal?.throwIfAborted();
|
|
@@ -156,7 +163,7 @@ export function schedulePiToolCandidates(
|
|
|
156
163
|
name: "update_schedule",
|
|
157
164
|
label: "Update schedule",
|
|
158
165
|
description:
|
|
159
|
-
"Update one scheduled task without changing its id or history. Required input: id. Optional prompt, trigger, label and timezone preserve the saved values when omitted
|
|
166
|
+
"Update one scheduled task without changing its id or history. Required input: id. Optional prompt, trigger, label and timezone preserve the saved values when omitted; trigger uses the same `kind`-based shapes as `schedule`. Every edit requires user confirmation; changing Requirement requires a fresh Host authorization check. Call `list_schedules` first and use an exact id. If multiple tasks plausibly match the user's wording, ask which one instead of guessing.",
|
|
160
167
|
parameters: updateScheduleParameters,
|
|
161
168
|
async execute(_toolCallId, input, signal) {
|
|
162
169
|
signal?.throwIfAborted();
|
|
@@ -255,8 +262,8 @@ export function schedulePiToolCandidates(
|
|
|
255
262
|
if (!confirmation) return [];
|
|
256
263
|
return [{ ...toolCandidate, alwaysRequiresApproval: true, confirmation: {
|
|
257
264
|
async prepare(args, { intentId }) {
|
|
258
|
-
validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
|
|
259
|
-
const input =
|
|
265
|
+
const validated = validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
|
|
266
|
+
const input = validated as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
|
|
260
267
|
if (input.timezone !== undefined && !isValidTimeZone(input.timezone)) throw new Error("A valid IANA timezone is required");
|
|
261
268
|
if (input.trigger?.kind === "local" && input.timezone) {
|
|
262
269
|
const error = validateScheduleRuleSpec(toScheduleRuleSpec(input.trigger, input.timezone));
|
|
@@ -270,7 +277,7 @@ export function schedulePiToolCandidates(
|
|
|
270
277
|
});
|
|
271
278
|
if (prepared.id !== input.id) throw new Error("Host preparation changed the target Task ID");
|
|
272
279
|
const fullInput = { prompt: prepared.prompt, label: prepared.label, trigger: prepared.trigger, timezone: prepared.timezone };
|
|
273
|
-
validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
|
|
280
|
+
void validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
|
|
274
281
|
if (!prepared.label?.trim() || !isValidTimeZone(prepared.timezone) || !prepared.candidateId?.trim() || !prepared.candidateVersion?.trim() || !prepared.authorization?.version?.trim() || !prepared.authorization.scope?.trim() || !Array.isArray(prepared.authorization.missing)) {
|
|
275
282
|
throw new Error("Host preparation is incomplete");
|
|
276
283
|
}
|