@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.5",
3
+ "version": "0.3.0-alpha.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -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
- return this.currentAssemblyPin();
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
- 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/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
+ }
@@ -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: snapshot.profile.memory,
498
- memoryPort: snapshot.bindings.memory,
499
- 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,
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: "oldestFirst",
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> {
@@ -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>;
@@ -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
- ], { description: "When to fire." });
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. 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.",
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 = args as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
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
  }