@anhlp/alp-runtime 0.0.1-beta.3 → 0.0.1-beta.4

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.
@@ -615,6 +615,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
615
615
  required: string[];
616
616
  additionalProperties: boolean;
617
617
  };
618
+ } | {
619
+ type: string;
620
+ name: string;
621
+ description: string;
622
+ inputSchema: {
623
+ type: string;
624
+ properties: {
625
+ action: {
626
+ type: string;
627
+ enum: string[];
628
+ };
629
+ dueAt: {
630
+ type: string;
631
+ description: string;
632
+ };
633
+ prompt: {
634
+ type: string;
635
+ description: string;
636
+ };
637
+ requestId: {
638
+ type: string;
639
+ description: string;
640
+ };
641
+ id: {
642
+ type: string;
643
+ description: string;
644
+ };
645
+ status: {
646
+ type: string;
647
+ enum: string[];
648
+ };
649
+ };
650
+ required: string[];
651
+ additionalProperties: boolean;
652
+ };
618
653
  } | {
619
654
  type: string;
620
655
  name: string;
@@ -1265,6 +1300,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
1265
1300
  required: string[];
1266
1301
  additionalProperties: boolean;
1267
1302
  };
1303
+ } | {
1304
+ type: string;
1305
+ name: string;
1306
+ description: string;
1307
+ inputSchema: {
1308
+ type: string;
1309
+ properties: {
1310
+ action: {
1311
+ type: string;
1312
+ enum: string[];
1313
+ };
1314
+ dueAt: {
1315
+ type: string;
1316
+ description: string;
1317
+ };
1318
+ prompt: {
1319
+ type: string;
1320
+ description: string;
1321
+ };
1322
+ requestId: {
1323
+ type: string;
1324
+ description: string;
1325
+ };
1326
+ id: {
1327
+ type: string;
1328
+ description: string;
1329
+ };
1330
+ status: {
1331
+ type: string;
1332
+ enum: string[];
1333
+ };
1334
+ };
1335
+ required: string[];
1336
+ additionalProperties: boolean;
1337
+ };
1268
1338
  } | {
1269
1339
  type: string;
1270
1340
  name: string;
@@ -1917,6 +1987,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
1917
1987
  required: string[];
1918
1988
  additionalProperties: boolean;
1919
1989
  };
1990
+ } | {
1991
+ type: string;
1992
+ name: string;
1993
+ description: string;
1994
+ inputSchema: {
1995
+ type: string;
1996
+ properties: {
1997
+ action: {
1998
+ type: string;
1999
+ enum: string[];
2000
+ };
2001
+ dueAt: {
2002
+ type: string;
2003
+ description: string;
2004
+ };
2005
+ prompt: {
2006
+ type: string;
2007
+ description: string;
2008
+ };
2009
+ requestId: {
2010
+ type: string;
2011
+ description: string;
2012
+ };
2013
+ id: {
2014
+ type: string;
2015
+ description: string;
2016
+ };
2017
+ status: {
2018
+ type: string;
2019
+ enum: string[];
2020
+ };
2021
+ };
2022
+ required: string[];
2023
+ additionalProperties: boolean;
2024
+ };
1920
2025
  } | {
1921
2026
  type: string;
1922
2027
  name: string;
@@ -5,7 +5,7 @@ import { anyModel, profileModel } from "@anhlp/alp-core/profiles";
5
5
  import { models, thinkingOptionsFor } from "./catalog.js";
6
6
  import { claudeSandboxAvailable, SKILL_PLUGIN } from "./claude-transport.js";
7
7
  import { languageInstruction } from "./language.js";
8
- import { HANDOFF_TOOL, WAIT_TOOL, SEND_TOOL, MERGE_TOOL, VERIFY_TOOL, DISCARD_TOOL, CANCEL_TOOL, RECALL_TOOL, PIN_TOOL, BOARD_TOOL, UNPIN_TOOL, ASK_TOOL, LESSON_TOOL, SKILL_TOOL, ISSUE_TOOL, mainOf, taskActions, taskTool } from "./tools.js";
8
+ import { HANDOFF_TOOL, WAIT_TOOL, SEND_TOOL, MERGE_TOOL, VERIFY_TOOL, DISCARD_TOOL, CANCEL_TOOL, RECALL_TOOL, PIN_TOOL, BOARD_TOOL, UNPIN_TOOL, ASK_TOOL, LESSON_TOOL, SKILL_TOOL, ISSUE_TOOL, SCHEDULE_TOOL, mainOf, taskActions, taskTool } from "./tools.js";
9
9
  import { COMPACT_SHARE } from "./runtime-context.js";
10
10
  var nativeMode = (mapping) => mapping.runtimeKind === "antigravity" && mapping.mode === "read-only" && mapping.antigravity?.readOnlyMode === "instructions" ? "full-access" : mapping.copy ? "workspace-write" : mapping.mode;
11
11
  function claudeFloor(mapping) {
@@ -182,7 +182,7 @@ function nativeSessionConfig(runtimeKind, mapping, targets, parentAgent, role, s
182
182
  ...targets.length || parentAgent ? [SEND_TOOL] : [],
183
183
  ...parentAgent ? [HANDOFF_TOOL] : [],
184
184
  ...supervised ? [LESSON_TOOL, SKILL_TOOL] : [],
185
- ...!parentAgent && mapping.agent.name === mainOf(mapping) ? [ISSUE_TOOL] : [],
185
+ ...!parentAgent && mapping.agent.name === mainOf(mapping) ? [ISSUE_TOOL, SCHEDULE_TOOL] : [],
186
186
  ASK_TOOL,
187
187
  PIN_TOOL,
188
188
  BOARD_TOOL,
@@ -1,15 +1,28 @@
1
- import type { RuntimeTransport } from './contracts.js';
1
+ import type { NativeBase, RuntimeTransport } from './contracts.js';
2
2
  import type { RuntimeKind, SessionSpec, ResolvedSession } from './resolve.js';
3
3
  import type { MailEvent } from './mailbox.js';
4
4
  import type { Worktree, WorktreeChange, Copy } from './workspace.js';
5
5
  import type { Verification } from '@anhlp/alp-core/verify';
6
6
  import type { Handoff } from './handoff.js';
7
7
  import type { SessionUsage } from './events.js';
8
+ /** The pause that stopped a turn; a matching resume may arrive before its interruption settles. */
9
+ export type ParkPause = {
10
+ scope: RuntimeKind | 'all';
11
+ resumed?: boolean;
12
+ };
13
+ export type Parked = {
14
+ reason: string;
15
+ since: number;
16
+ prompt?: string;
17
+ pause?: ParkPause;
18
+ };
8
19
  export type Session = {
9
20
  runtimeKind: RuntimeKind;
10
21
  runtime: RuntimeTransport;
11
22
  mapping: ResolvedSession;
12
23
  threadId: string;
24
+ /** Claude: the conversation threadId is made from on its first prompt, until then (a fork or rewind, §12.7). */
25
+ base?: NativeBase;
13
26
  active?: string;
14
27
  /** A continued turn accepts a simultaneous user auto prompt as steering. */
15
28
  resumedTurn?: string;
@@ -44,11 +57,7 @@ export type Session = {
44
57
  /** Set by interrupt: pending mail waits for the next user prompt. */
45
58
  wakeBlocked: boolean;
46
59
  /** An assignment a usage limit or a pause stopped: it waits, open, until its runtime resumes. prompt is what continues it. */
47
- parked?: {
48
- reason: string;
49
- since: number;
50
- prompt?: string;
51
- };
60
+ parked?: Parked;
52
61
  /** What the requester of a session parked by its turn's end is told happens next, instead of waiting for alp resume. */
53
62
  parkThen?: string;
54
63
  /** When ALP restarted this session's native process, within the last RESTART_WINDOW_MS. */
@@ -67,8 +76,15 @@ export type Session = {
67
76
  skillsUsed?: Set<string>;
68
77
  /** Why the running turn is being stopped, so its end parks the assignment instead of ending it. */
69
78
  parkReason?: string;
79
+ parkPause?: ParkPause;
70
80
  /** Mail arrived while its runtime was paused; it is delivered on resume. */
71
81
  wakeHeld?: boolean;
82
+ /** How many lifecycle operations (a rewind) hold its wakes now; mail waits until the last releases (AlpRuntime.hold). */
83
+ lifecycleHolds?: number;
84
+ /** Its parked work was to continue during a lifecycle hold; it continues when the last hold releases. */
85
+ resumeHeld?: Parked;
86
+ /** An assignment opened but not yet sent its brief: mail waits and rides with the brief's turn. */
87
+ awaitingBrief?: boolean;
72
88
  /** Permission questions to the user, one at a time. */
73
89
  permissionQueue?: Promise<unknown>;
74
90
  toolCalls: Map<string, Promise<unknown>>;
package/dist/tools.d.ts CHANGED
@@ -680,3 +680,40 @@ export declare function taskTool(actions: TaskAction[]): {
680
680
  };
681
681
  /** The task an assignment takes, as the start of its brief. */
682
682
  export declare function taskBrief(task: Task, writing: boolean): string;
683
+ /** Only the project's main root may schedule a future continuation. */
684
+ export declare const SCHEDULE_TOOL: {
685
+ type: string;
686
+ name: string;
687
+ description: string;
688
+ inputSchema: {
689
+ type: string;
690
+ properties: {
691
+ action: {
692
+ type: string;
693
+ enum: string[];
694
+ };
695
+ dueAt: {
696
+ type: string;
697
+ description: string;
698
+ };
699
+ prompt: {
700
+ type: string;
701
+ description: string;
702
+ };
703
+ requestId: {
704
+ type: string;
705
+ description: string;
706
+ };
707
+ id: {
708
+ type: string;
709
+ description: string;
710
+ };
711
+ status: {
712
+ type: string;
713
+ enum: string[];
714
+ };
715
+ };
716
+ required: string[];
717
+ additionalProperties: boolean;
718
+ };
719
+ };
package/dist/tools.js CHANGED
@@ -348,6 +348,24 @@ function taskBrief(task, writing) {
348
348
  "Your handoff moves this task to review for your requester to accept. List work you found outside the task under discovered; only main and the user create tasks."
349
349
  ].join("\n");
350
350
  }
351
+ var SCHEDULE_TOOL = {
352
+ type: "function",
353
+ name: "alp_schedule",
354
+ description: "Create, list, inspect or cancel a one-shot continuation of this main/root session. create proposes the exact prompt and ISO time with UTC offset for user approval. Jobs wake an idle main without a user message; timer gates only unblock tasks, and assignment notes do not schedule a wake. Scheduling a quota-reset continuation never guarantees provider availability: recheck actual availability before delegating.",
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {
358
+ action: { type: "string", enum: ["create", "list", "get", "cancel"] },
359
+ dueAt: { type: "string", description: "For create: ISO timestamp with Z or an explicit UTC offset." },
360
+ prompt: { type: "string", description: "For create: the exact continuation, at most 8000 characters." },
361
+ requestId: { type: "string", description: "For create: stable id for this approval proposal; reuse only for an identical draft." },
362
+ id: { type: "string", description: "For get/cancel: the scheduled job id." },
363
+ status: { type: "string", enum: ["pending", "delivered", "failed", "cancelled"] }
364
+ },
365
+ required: ["action"],
366
+ additionalProperties: false
367
+ }
368
+ };
351
369
  export {
352
370
  ASK_TOOL,
353
371
  BOARD_TOOL,
@@ -359,6 +377,7 @@ export {
359
377
  MERGE_TOOL,
360
378
  PIN_TOOL,
361
379
  RECALL_TOOL,
380
+ SCHEDULE_TOOL,
362
381
  SEND_TOOL,
363
382
  SKILL_TOOL,
364
383
  TASK_ACTIONS,
@@ -4,6 +4,7 @@ import type { RuntimeOptions } from './contracts.js';
4
4
  import type { Session, Assignment } from './session-state.js';
5
5
  type WorktreeContext = {
6
6
  options: RuntimeOptions;
7
+ releaseResult(assignmentId: string): Promise<void>;
7
8
  leases: Map<string, {
8
9
  assignment: string;
9
10
  agent: string;
@@ -15,7 +16,7 @@ type WorktreeContext = {
15
16
  touchTask(rootId: string, id: string): void;
16
17
  };
17
18
  /** Checkout mutation and verification retain one shared queue per checkout. */
18
- export declare function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }: WorktreeContext): {
19
+ export declare function createWorktreeDomain({ options, leases, releaseResult, rootOf, lineage, runLog, runHooks, touchTask }: WorktreeContext): {
19
20
  settleWorktree: (parentId: string, parent: Session, assignment: Assignment) => Promise<{
20
21
  branch: string;
21
22
  files: never[];
@@ -8,7 +8,7 @@ import { writes } from "./catalog.js";
8
8
  import { nativeEnvironment } from "./providers.js";
9
9
  import { mainOf } from "./tools.js";
10
10
  import { errorData, plainObject, toolResult } from "./tool-results.js";
11
- function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }) {
11
+ function createWorktreeDomain({ options, leases, releaseResult, rootOf, lineage, runLog, runHooks, touchTask }) {
12
12
  const merging = /* @__PURE__ */ new Map();
13
13
  async function settleWorktree(parentId, parent, assignment) {
14
14
  const worktree = assignment.worktree;
@@ -16,6 +16,7 @@ function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHoo
16
16
  const change = await commitWorktree(worktree, `alp: ${assignment.agent} assignment ${assignment.id}`);
17
17
  if (!change.files.length) {
18
18
  await removeWorktree(worktree, { deleteBranch: true });
19
+ await releaseResult(assignment.id);
19
20
  return { branch: worktree.branch, files: [], note: "No changes; the worktree was removed" };
20
21
  }
21
22
  const described = { branch: worktree.branch, base: worktree.base, commit: change.commit, files: change.files, stat: change.stat };
@@ -44,6 +45,7 @@ function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHoo
44
45
  if (action === "discard") {
45
46
  session.worktrees.delete(args.assignmentId);
46
47
  await removeWorktree(worktree, { deleteBranch: true });
48
+ await releaseResult(args.assignmentId);
47
49
  runLog(rootOf(sessionId), { event: "worktree.discarded", assignmentId: args.assignmentId, branch: worktree.branch });
48
50
  return toolResult(true, { discarded: args.assignmentId, branch: worktree.branch });
49
51
  }
@@ -104,6 +106,7 @@ ${refused}`, branch: worktree.branch, next: `Fix what it reports in a new assign
104
106
  return toolResult(false, { error: errorData(error).message, branch: worktree.branch, next: "Merge the branch yourself, or alp_discard it" });
105
107
  }
106
108
  await removeWorktree(worktree, { deleteBranch: merged.status !== "conflicts" });
109
+ if (merged.status !== "conflicts") await releaseResult(args.assignmentId);
107
110
  runLog(rootId, { event: "worktree.merged", assignmentId: args.assignmentId, branch: worktree.branch, ...merged });
108
111
  let checked;
109
112
  if (config && args.skipVerify === void 0 && merged.status === "applied" && moved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anhlp/alp-runtime",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.4",
4
4
  "type": "module",
5
5
  "description": "ALP runtime module",
6
6
  "files": [
@@ -25,6 +25,14 @@
25
25
  "types": "./dist/antigravity-transport.d.ts",
26
26
  "default": "./dist/antigravity-transport.js"
27
27
  },
28
+ "./attachments": {
29
+ "alp-source": {
30
+ "types": "./src/attachments.ts",
31
+ "default": "./src/attachments.ts"
32
+ },
33
+ "types": "./dist/attachments.d.ts",
34
+ "default": "./dist/attachments.js"
35
+ },
28
36
  "./board": {
29
37
  "alp-source": {
30
38
  "types": "./src/board.ts",
@@ -222,8 +230,8 @@
222
230
  "node": ">=22.12"
223
231
  },
224
232
  "dependencies": {
225
- "@anhlp/alp-core": "0.0.1-beta.3",
226
- "@anhlp/alp-protocol": "0.0.1-beta.3",
233
+ "@anhlp/alp-core": "0.0.1-beta.4",
234
+ "@anhlp/alp-protocol": "0.0.1-beta.4",
227
235
  "@anthropic-ai/claude-agent-sdk": "^0.3.292",
228
236
  "@modelcontextprotocol/sdk": "^1.32.1",
229
237
  "@agentclientprotocol/sdk": "1.7.0",
@@ -0,0 +1,64 @@
1
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeAtomic } from '@anhlp/alp-core/atomic';
4
+
5
+ export type AssignmentResultOwner = { assignmentId: string; checkout: string; worktree?: string };
6
+ type Ledger = { version: 1; workspaces: string[]; results: AssignmentResultOwner[] };
7
+ const empty = (): Ledger => ({ version: 1, workspaces: [], results: [] });
8
+ const absolute = (value: unknown): value is string => typeof value === 'string' && path.isAbsolute(value);
9
+ const valid = (value: any): value is Ledger => value?.version === 1 && Array.isArray(value.workspaces) && value.workspaces.every(absolute) &&
10
+ Array.isArray(value.results) && value.results.every((entry: any) => entry && typeof entry.assignmentId === 'string' && absolute(entry.checkout) && (entry.worktree === undefined || absolute(entry.worktree)));
11
+
12
+ /** Permanent ownership, independent of native-thread recall expiry. Missing initialized data is never recreated. */
13
+ export function createAssignmentResults(file?: string) {
14
+ let memory = empty();
15
+ let queue = Promise.resolve();
16
+ const marker = file ? `${file}.initialized` : undefined;
17
+ async function load(initialize = false): Promise<Ledger> {
18
+ if (!file) return memory;
19
+ let value: unknown;
20
+ try { value = JSON.parse(await readFile(file, 'utf8')); }
21
+ catch (error) {
22
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || !initialize) throw new Error(`Cannot inspect assignment result ledger ${file}`);
23
+ try { await readFile(marker!, 'utf8'); }
24
+ catch (missing) {
25
+ if ((missing as NodeJS.ErrnoException).code === 'ENOENT') return empty();
26
+ throw missing;
27
+ }
28
+ throw new Error(`Assignment result ledger ${file} was lost; restore it before managing workspaces`);
29
+ }
30
+ if (!valid(value)) throw new Error(`Invalid assignment result ledger ${file}`);
31
+ return value;
32
+ }
33
+ async function save(value: Ledger) {
34
+ if (!file) { memory = value; return; }
35
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
36
+ await writeAtomic(file, JSON.stringify(value) + '\n');
37
+ // The marker survives a lost ledger; registration must never silently reset ownership.
38
+ await writeFile(marker!, '1\n', { flag: 'wx', mode: 0o600 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
39
+ }
40
+ function serialize<T>(run: () => Promise<T>): Promise<T> {
41
+ const result = queue.then(run);
42
+ queue = result.then(() => {}, () => {});
43
+ return result;
44
+ }
45
+ return {
46
+ register(directory: string) { return serialize(async () => {
47
+ const value = await load(true);
48
+ if (!value.workspaces.includes(directory)) value.workspaces.push(directory);
49
+ await save(value);
50
+ }); },
51
+ put(entry: AssignmentResultOwner) { return serialize(async () => {
52
+ // Initial assignment activity can establish the index, but never certify old workspaces.
53
+ const value = await load(true);
54
+ value.results = [...value.results.filter(existing => existing.assignmentId !== entry.assignmentId), entry];
55
+ await save(value);
56
+ }); },
57
+ remove(assignmentId: string) { return serialize(async () => {
58
+ const value = await load();
59
+ value.results = value.results.filter(entry => entry.assignmentId !== assignmentId);
60
+ await save(value);
61
+ }); },
62
+ inspect() { return serialize(() => load()); },
63
+ };
64
+ }