@sigrun/shared 0.0.0-stage → 0.1.1
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/access.d.ts +55 -0
- package/dist/access.js +72 -0
- package/dist/ambient.d.ts +111 -0
- package/dist/ambient.js +23 -0
- package/dist/blocks/format.d.ts +28 -0
- package/dist/blocks/format.js +199 -0
- package/dist/blocks/icons.d.ts +12 -0
- package/dist/blocks/icons.js +64 -0
- package/dist/blocks/index.d.ts +24 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/lttb.d.ts +16 -0
- package/dist/blocks/lttb.js +67 -0
- package/dist/blocks/ops.d.ts +44 -0
- package/dist/blocks/ops.js +294 -0
- package/dist/blocks/text.d.ts +43 -0
- package/dist/blocks/text.js +790 -0
- package/dist/blocks/transport.d.ts +77 -0
- package/dist/blocks/transport.js +11 -0
- package/dist/blocks/types.d.ts +541 -0
- package/dist/blocks/types.js +17 -0
- package/dist/commands.d.ts +75 -0
- package/dist/commands.js +25 -0
- package/dist/conversations.d.ts +31 -0
- package/dist/conversations.js +94 -0
- package/dist/interactions.d.ts +91 -0
- package/dist/interactions.js +21 -0
- package/dist/jobs.d.ts +109 -0
- package/dist/jobs.js +19 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +13 -0
- package/dist/live.d.ts +71 -0
- package/dist/live.js +82 -0
- package/dist/session-meta.d.ts +119 -0
- package/dist/session-meta.js +13 -0
- package/dist/settings.d.ts +408 -0
- package/dist/settings.js +19 -0
- package/dist/themes.d.ts +50 -0
- package/dist/themes.js +156 -0
- package/dist/turns.d.ts +119 -0
- package/dist/turns.js +197 -0
- package/dist/view-delta.d.ts +54 -0
- package/dist/view-delta.js +135 -0
- package/dist/views.d.ts +784 -0
- package/dist/views.js +38 -0
- package/package.json +90 -5
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slash-command WIRE types (Task 7, pi parity) — the client-facing
|
|
3
|
+
* half of `src/server/commands.ts` (which owns the catalog runtime).
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the shared leaf package (`@sigrun/shared`): the
|
|
6
|
+
* tui and webui import these shapes from here, never from server source
|
|
7
|
+
* (the load-bearing decoupling — the tui is a pure client of the wire).
|
|
8
|
+
*
|
|
9
|
+
* PURE module: no imports at all (the views-purity guard enforces this
|
|
10
|
+
* across `shared/src`).
|
|
11
|
+
*
|
|
12
|
+
* Registry header (from the server module — the catalog itself lives
|
|
13
|
+
* there): a THIN registry of command METADATA — name, description, args
|
|
14
|
+
* hint, and the §5 endpoint each command drives. Deliberately NO
|
|
15
|
+
* server-side `execute` indirection: the composer owns the client calls,
|
|
16
|
+
* the registry only tells it what exists and where it goes. Scope
|
|
17
|
+
* (controller rulings): ONLY commands mapping to EXISTING endpoints.
|
|
18
|
+
* `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
|
|
19
|
+
* cross the API); `/skill:name` and prompt templates are Task 2c's
|
|
20
|
+
* `kind: "expand"` entries (no endpoint — the server expands the text
|
|
21
|
+
* at admission). `/model` and `/thinking` ARE included: both map
|
|
22
|
+
* cleanly onto `POST .../configure`'s existing
|
|
23
|
+
* `{model?, thinking?}` body (each sends exactly one of the two fields).
|
|
24
|
+
*/
|
|
25
|
+
/** The HTTP method + literal route path a command drives. */
|
|
26
|
+
export interface SlashCommandEndpoint {
|
|
27
|
+
readonly method: "GET" | "POST" | "PATCH";
|
|
28
|
+
/**
|
|
29
|
+
* The REGISTERED route path (Fastify `:param` form, e.g.
|
|
30
|
+
* `/v1/conversations/:id/fork`) — the composer substitutes the active
|
|
31
|
+
* conversation id; `server-commands.test.ts` asserts every entry
|
|
32
|
+
* resolves against the live route table.
|
|
33
|
+
*/
|
|
34
|
+
readonly path: string;
|
|
35
|
+
}
|
|
36
|
+
/** One slash command's catalog entry. */
|
|
37
|
+
export interface SlashCommand {
|
|
38
|
+
/** The bare name (no leading slash) — the `Map` key. */
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly description: string;
|
|
41
|
+
/** Argument shape shown in the menu/usage errors; absent = no args. */
|
|
42
|
+
readonly argsHint?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The contributing extension's slug (W5 Task 3); ABSENT on core
|
|
45
|
+
* commands. An extension entry's `endpoint` is its invoke route
|
|
46
|
+
* (`POST /v1/conversations/:id/commands/<name>`) — the client POSTs
|
|
47
|
+
* there with `{args}` and renders a returned `notice`.
|
|
48
|
+
*/
|
|
49
|
+
readonly extension?: string;
|
|
50
|
+
/**
|
|
51
|
+
* How the composer satisfies the command. ABSENT means `"endpoint"`
|
|
52
|
+
* (every pre-existing entry — the composer calls `endpoint`);
|
|
53
|
+
* `"expand"` (Task 2c) means the client inserts `/name ` into the
|
|
54
|
+
* composer and the SERVER expands the submitted text at admission
|
|
55
|
+
* (`/skill:name` and prompt templates) — no endpoint exists and none
|
|
56
|
+
* is needed.
|
|
57
|
+
*/
|
|
58
|
+
readonly kind?: "endpoint" | "expand";
|
|
59
|
+
/** The endpoint an `endpoint`-kind entry drives; absent when `kind === "expand"`. */
|
|
60
|
+
readonly endpoint?: SlashCommandEndpoint;
|
|
61
|
+
}
|
|
62
|
+
/** The `GET /v1/commands` response (spec §5 response conventions). */
|
|
63
|
+
export interface CommandsResponse {
|
|
64
|
+
readonly commands: readonly SlashCommand[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `POST /v1/conversations/:id/commands/:name` response (W5 Task 3,
|
|
68
|
+
* extension commands): the command's optional human-facing notice —
|
|
69
|
+
* the client renders it as a transient banner (webui) / status-bar
|
|
70
|
+
* flash (tui). An empty body means the command returned nothing to
|
|
71
|
+
* say.
|
|
72
|
+
*/
|
|
73
|
+
export interface CommandRunResponse {
|
|
74
|
+
readonly notice?: string;
|
|
75
|
+
}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slash-command WIRE types (Task 7, pi parity) — the client-facing
|
|
3
|
+
* half of `src/server/commands.ts` (which owns the catalog runtime).
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the shared leaf package (`@sigrun/shared`): the
|
|
6
|
+
* tui and webui import these shapes from here, never from server source
|
|
7
|
+
* (the load-bearing decoupling — the tui is a pure client of the wire).
|
|
8
|
+
*
|
|
9
|
+
* PURE module: no imports at all (the views-purity guard enforces this
|
|
10
|
+
* across `shared/src`).
|
|
11
|
+
*
|
|
12
|
+
* Registry header (from the server module — the catalog itself lives
|
|
13
|
+
* there): a THIN registry of command METADATA — name, description, args
|
|
14
|
+
* hint, and the §5 endpoint each command drives. Deliberately NO
|
|
15
|
+
* server-side `execute` indirection: the composer owns the client calls,
|
|
16
|
+
* the registry only tells it what exists and where it goes. Scope
|
|
17
|
+
* (controller rulings): ONLY commands mapping to EXISTING endpoints.
|
|
18
|
+
* `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
|
|
19
|
+
* cross the API); `/skill:name` and prompt templates are Task 2c's
|
|
20
|
+
* `kind: "expand"` entries (no endpoint — the server expands the text
|
|
21
|
+
* at admission). `/model` and `/thinking` ARE included: both map
|
|
22
|
+
* cleanly onto `POST .../configure`'s existing
|
|
23
|
+
* `{model?, thinking?}` body (each sends exactly one of the two fields).
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation list tree (Task B1, UI refresh): sidebar shaping —
|
|
3
|
+
* fork nesting, recency ordering, pinned grouping and age rendering.
|
|
4
|
+
*
|
|
5
|
+
* PURE module: no imports at all (test/views-purity.test.ts enforces
|
|
6
|
+
* this). `ListRow` is structural — both clients' wire row types fit —
|
|
7
|
+
* and the only fields read are the ones the list endpoint ships.
|
|
8
|
+
*/
|
|
9
|
+
export interface ListRow {
|
|
10
|
+
readonly id: number;
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
readonly lastActivityAt?: string;
|
|
13
|
+
readonly parent?: {
|
|
14
|
+
readonly id: number;
|
|
15
|
+
};
|
|
16
|
+
readonly archived: boolean;
|
|
17
|
+
readonly pinned: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface TreeNode<R extends ListRow> {
|
|
20
|
+
readonly row: R;
|
|
21
|
+
readonly children: readonly TreeNode<R>[];
|
|
22
|
+
/** Max epoch ms over the whole subtree; 0 when nothing was ever used. */
|
|
23
|
+
readonly newestActivity: number;
|
|
24
|
+
}
|
|
25
|
+
export type AgeGroup = "Pinned" | "Today" | "Yesterday" | "Earlier";
|
|
26
|
+
export declare function buildConversationTree<R extends ListRow>(rows: readonly R[], options: {
|
|
27
|
+
includeArchived: boolean;
|
|
28
|
+
}): TreeNode<R>[];
|
|
29
|
+
export declare function ageGroup(node: TreeNode<ListRow>, now: Date): AgeGroup;
|
|
30
|
+
export declare function formatAge(iso: string | undefined, now: Date): string;
|
|
31
|
+
export declare function displayName(row: ListRow): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation list tree (Task B1, UI refresh): sidebar shaping —
|
|
3
|
+
* fork nesting, recency ordering, pinned grouping and age rendering.
|
|
4
|
+
*
|
|
5
|
+
* PURE module: no imports at all (test/views-purity.test.ts enforces
|
|
6
|
+
* this). `ListRow` is structural — both clients' wire row types fit —
|
|
7
|
+
* and the only fields read are the ones the list endpoint ships.
|
|
8
|
+
*/
|
|
9
|
+
/** Sort: newest first, ties by id descending. */
|
|
10
|
+
function byRecency(a, b) {
|
|
11
|
+
return b.newestActivity - a.newestActivity || b.row.id - a.row.id;
|
|
12
|
+
}
|
|
13
|
+
/** Epoch ms of a row's activity stamp; 0 when absent or unparsable. */
|
|
14
|
+
function epoch(iso) {
|
|
15
|
+
if (iso === undefined)
|
|
16
|
+
return 0;
|
|
17
|
+
const parsed = Date.parse(iso);
|
|
18
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
19
|
+
}
|
|
20
|
+
export function buildConversationTree(rows, options) {
|
|
21
|
+
const nodes = new Map();
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
// `parent` is fork lineage — an ancestor by construction (set once,
|
|
24
|
+
// at fork time), so the resulting forest is acyclic.
|
|
25
|
+
if (options.includeArchived || !row.archived) {
|
|
26
|
+
nodes.set(row.id, { row, children: [] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const roots = [];
|
|
30
|
+
for (const node of nodes.values()) {
|
|
31
|
+
const parent = node.row.parent;
|
|
32
|
+
const parentNode = parent === undefined ? undefined : nodes.get(parent.id);
|
|
33
|
+
if (parentNode === undefined || parentNode === node) {
|
|
34
|
+
roots.push(node);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
parentNode.children.push(node);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const toNode = (node) => {
|
|
41
|
+
const children = node.children.map(toNode).sort(byRecency);
|
|
42
|
+
const own = epoch(node.row.lastActivityAt);
|
|
43
|
+
const newestActivity = children.reduce((max, child) => Math.max(max, child.newestActivity), own);
|
|
44
|
+
return { row: node.row, children, newestActivity };
|
|
45
|
+
};
|
|
46
|
+
return roots
|
|
47
|
+
.map(toNode)
|
|
48
|
+
.sort((a, b) => Number(b.row.pinned) - Number(a.row.pinned) || byRecency(a, b));
|
|
49
|
+
}
|
|
50
|
+
/** Local-calendar-day distance between two instants. */
|
|
51
|
+
function dayDistance(then, now) {
|
|
52
|
+
const midnight = (value) => {
|
|
53
|
+
const date = new Date(value);
|
|
54
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
55
|
+
};
|
|
56
|
+
// `round` absorbs DST's 23/25-hour days around the midnight anchor.
|
|
57
|
+
return Math.round((midnight(now) - midnight(then)) / 86_400_000);
|
|
58
|
+
}
|
|
59
|
+
export function ageGroup(node, now) {
|
|
60
|
+
if (node.row.pinned)
|
|
61
|
+
return "Pinned";
|
|
62
|
+
const days = dayDistance(node.newestActivity, now);
|
|
63
|
+
if (days <= 0)
|
|
64
|
+
return "Today";
|
|
65
|
+
if (days === 1)
|
|
66
|
+
return "Yesterday";
|
|
67
|
+
return "Earlier";
|
|
68
|
+
}
|
|
69
|
+
const MINUTE = 60_000;
|
|
70
|
+
const HOUR = 3_600_000;
|
|
71
|
+
const DAY = 86_400_000;
|
|
72
|
+
export function formatAge(iso, now) {
|
|
73
|
+
if (iso === undefined)
|
|
74
|
+
return "";
|
|
75
|
+
const at = Date.parse(iso);
|
|
76
|
+
if (Number.isNaN(at))
|
|
77
|
+
return "";
|
|
78
|
+
const age = now.getTime() - at;
|
|
79
|
+
if (age < MINUTE)
|
|
80
|
+
return "now";
|
|
81
|
+
if (age < HOUR)
|
|
82
|
+
return `${Math.floor(age / MINUTE)}m`;
|
|
83
|
+
if (age < DAY)
|
|
84
|
+
return `${Math.floor(age / HOUR)}h`;
|
|
85
|
+
if (age < 7 * DAY)
|
|
86
|
+
return `${Math.floor(age / DAY)}d`;
|
|
87
|
+
return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(at);
|
|
88
|
+
}
|
|
89
|
+
export function displayName(row) {
|
|
90
|
+
const flattened = row.name?.replace(/[\r\n]+/g, " ").trim();
|
|
91
|
+
return flattened === undefined || flattened.length === 0
|
|
92
|
+
? "New conversation"
|
|
93
|
+
: flattened;
|
|
94
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
|
|
3
|
+
* channel-neutral record shapes for "this conversation is waiting on a
|
|
4
|
+
* human decision", written by extensions through the host context and
|
|
5
|
+
* answered by channels through one generic route.
|
|
6
|
+
*
|
|
7
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
8
|
+
* `src/harness/interactions.ts` imports them (type-only) for the doc
|
|
9
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
10
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
11
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
12
|
+
*
|
|
13
|
+
* Layering (strict envelope / closed primitives / opaque details):
|
|
14
|
+
*
|
|
15
|
+
* - envelope: kind, callId, status, timestamps, answer — load-bearing for
|
|
16
|
+
* joining tool slots and lifecycle arbitration. Strictly validated.
|
|
17
|
+
* - body: composed ONLY from the closed primitives (text, options,
|
|
18
|
+
* fields, diff). Structurally validated; unknown body keys quarantine.
|
|
19
|
+
* - details: opaque extension-owned JSON. Never interpreted.
|
|
20
|
+
*/
|
|
21
|
+
import type { JsonObject } from "./json.js";
|
|
22
|
+
/** Lifecycle of one interaction record (spec statuses). */
|
|
23
|
+
export type InteractionStatus = "pending" | "answered" | "skipped" | "expired" | "cancelled";
|
|
24
|
+
/** One choice in the `options` primitive. */
|
|
25
|
+
export type InteractionOption = {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** Second line under the label (pi-advisor presets, rpiv options). */
|
|
29
|
+
readonly description?: string;
|
|
30
|
+
/** Channel-schemed color/severity hint: "danger" | "warn" | "ok" | "neutral". */
|
|
31
|
+
readonly tone?: "danger" | "warn" | "ok" | "neutral";
|
|
32
|
+
/** Pre-selected by the extension's recommendation (pi-interview conviction). */
|
|
33
|
+
readonly recommended?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/** One light-form field in the `fields` primitive. */
|
|
36
|
+
export type InteractionField = {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly type: "string" | "number" | "boolean" | "secret";
|
|
39
|
+
readonly label?: string;
|
|
40
|
+
readonly required?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** The `diff` primitive: before/after content, optionally path-scoped. */
|
|
43
|
+
export type InteractionDiff = {
|
|
44
|
+
readonly before: string;
|
|
45
|
+
readonly after: string;
|
|
46
|
+
readonly path?: string;
|
|
47
|
+
readonly lang?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The interaction body: ONLY these four keys, each built only from the
|
|
51
|
+
* closed primitives. Everything else belongs in `details`.
|
|
52
|
+
*/
|
|
53
|
+
export type InteractionBody = {
|
|
54
|
+
/** Prose prompt body (markdown). */
|
|
55
|
+
readonly text?: string;
|
|
56
|
+
readonly options?: InteractionOption[];
|
|
57
|
+
readonly fields?: InteractionField[];
|
|
58
|
+
readonly diff?: InteractionDiff;
|
|
59
|
+
};
|
|
60
|
+
/** What an answer may carry: chosen ids, form values, and/or free text. */
|
|
61
|
+
export type InteractionAnswer = {
|
|
62
|
+
readonly ids?: string[];
|
|
63
|
+
readonly values?: Readonly<Record<string, string | number | boolean>>;
|
|
64
|
+
readonly text?: string;
|
|
65
|
+
};
|
|
66
|
+
/** The strict envelope + closed-primitive body of one interaction. */
|
|
67
|
+
export type InteractionRecord = {
|
|
68
|
+
/** Extension-declared kind id (e.g. "gating.confirm", "questions.form"). */
|
|
69
|
+
readonly kind: string;
|
|
70
|
+
/** Stable join key to the pi.live tool slot (`ToolCall.id`). */
|
|
71
|
+
readonly callId: string;
|
|
72
|
+
readonly status: InteractionStatus;
|
|
73
|
+
/** Epoch ms when seeded. */
|
|
74
|
+
readonly createdAt: number;
|
|
75
|
+
/** Epoch ms after which channels may show it as expired. */
|
|
76
|
+
readonly expiresAt?: number;
|
|
77
|
+
/** Epoch ms when a terminal status landed. */
|
|
78
|
+
readonly settledAt?: number;
|
|
79
|
+
readonly answer?: InteractionAnswer;
|
|
80
|
+
/** Who answered: "human" via the route, or the extension itself. */
|
|
81
|
+
readonly answeredBy?: "human" | "extension";
|
|
82
|
+
/**
|
|
83
|
+
* The answering principal's directory username (spec §5.9): stamped by
|
|
84
|
+
* the answer route from `request.principal.user` — host-assigned, never
|
|
85
|
+
* model-asserted. Absent on the naive single-operator path (no user).
|
|
86
|
+
*/
|
|
87
|
+
readonly answeredByUser?: string;
|
|
88
|
+
readonly body?: InteractionBody;
|
|
89
|
+
/** Opaque extension-owned payload; rendered collapsed, never interpreted. */
|
|
90
|
+
readonly details?: JsonObject;
|
|
91
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
|
|
3
|
+
* channel-neutral record shapes for "this conversation is waiting on a
|
|
4
|
+
* human decision", written by extensions through the host context and
|
|
5
|
+
* answered by channels through one generic route.
|
|
6
|
+
*
|
|
7
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
8
|
+
* `src/harness/interactions.ts` imports them (type-only) for the doc
|
|
9
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
10
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
11
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
12
|
+
*
|
|
13
|
+
* Layering (strict envelope / closed primitives / opaque details):
|
|
14
|
+
*
|
|
15
|
+
* - envelope: kind, callId, status, timestamps, answer — load-bearing for
|
|
16
|
+
* joining tool slots and lifecycle arbitration. Strictly validated.
|
|
17
|
+
* - body: composed ONLY from the closed primitives (text, options,
|
|
18
|
+
* fields, diff). Structurally validated; unknown body keys quarantine.
|
|
19
|
+
* - details: opaque extension-owned JSON. Never interpreted.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §8.2 job wire types — THE jobs channel contract.
|
|
3
|
+
*
|
|
4
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server owns
|
|
5
|
+
* the runtime half (src/jobs/* — validation, schedule math) and both
|
|
6
|
+
* clients (webui, tui) import these types directly. Like every wire
|
|
7
|
+
* module, imports are TYPE-ONLY from sibling shared modules (the
|
|
8
|
+
* views-purity guard enforces this), so the module erases at compile
|
|
9
|
+
* time and contributes no runtime.
|
|
10
|
+
*
|
|
11
|
+
* The shapes follow spec §8.2's schedule/action grammar verbatim:
|
|
12
|
+
* a schedule is exactly one of a 5-field `cron` (with an optional IANA
|
|
13
|
+
* `timezone`, defaulting to the server timezone), an `every` interval
|
|
14
|
+
* (`^\\d+(s|m|h|d)$`, minimum 60 s), or a one-shot ISO 8601 `at`; an
|
|
15
|
+
* action is a `prompt` (optionally pinning the model/thinking of a NEW
|
|
16
|
+
* conversation, or targeting a fixed conversation id) or a `task`
|
|
17
|
+
* (a registered extension task name plus optional JSON input).
|
|
18
|
+
*/
|
|
19
|
+
import type { Sharing } from "./access.js";
|
|
20
|
+
import type { JsonValue } from "./json.js";
|
|
21
|
+
export type { JsonValue };
|
|
22
|
+
/** When a job fires: a cron (5-field, TZ-aware), an interval, or one instant. */
|
|
23
|
+
export type JobSchedule = {
|
|
24
|
+
cron: string;
|
|
25
|
+
timezone?: string;
|
|
26
|
+
} | {
|
|
27
|
+
every: string;
|
|
28
|
+
} | {
|
|
29
|
+
at: string;
|
|
30
|
+
};
|
|
31
|
+
/** What a job does when it fires (spec §8.2). */
|
|
32
|
+
export type JobAction = {
|
|
33
|
+
kind: "prompt";
|
|
34
|
+
prompt: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
thinking?: string;
|
|
37
|
+
conversation: "new" | number;
|
|
38
|
+
runSharing?: Sharing;
|
|
39
|
+
/**
|
|
40
|
+
* Gateway delivery targets (spec §8.0 C7): 1–8 strings, each
|
|
41
|
+
* 1–256 chars, valid ONLY with `conversation: "new"` — a
|
|
42
|
+
* conversation bound to a chat already delivers there. Core
|
|
43
|
+
* validates SHAPE only; the target grammar belongs to the
|
|
44
|
+
* gateway. `fire.ts` stamps it onto the run conversation's
|
|
45
|
+
* SessionMeta so the gateway's runSettled delivery reads it.
|
|
46
|
+
*/
|
|
47
|
+
deliver?: string[];
|
|
48
|
+
} | {
|
|
49
|
+
kind: "task";
|
|
50
|
+
task: string;
|
|
51
|
+
input?: JsonValue;
|
|
52
|
+
};
|
|
53
|
+
/** A job's declared shape — what every source (api/file/extension) supplies. */
|
|
54
|
+
export type JobSpec = {
|
|
55
|
+
name: string;
|
|
56
|
+
schedule: JobSchedule;
|
|
57
|
+
action: JobAction;
|
|
58
|
+
overlap?: "skip" | "queue";
|
|
59
|
+
catchUp?: "once" | "none";
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
owner?: string;
|
|
62
|
+
sharing?: Sharing;
|
|
63
|
+
};
|
|
64
|
+
/** One job as `GET /v1/jobs` serves it (the list row and the detail). */
|
|
65
|
+
export type JobWire = {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
source: "api" | "file" | "extension";
|
|
69
|
+
owner?: string;
|
|
70
|
+
sharing?: Sharing;
|
|
71
|
+
schedule: JobSchedule;
|
|
72
|
+
action: JobAction;
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
state: "waiting" | "firing" | "disabled" | "blocked";
|
|
75
|
+
reason?: string;
|
|
76
|
+
version: number;
|
|
77
|
+
nextRun?: string;
|
|
78
|
+
lastRun?: JobRunWire;
|
|
79
|
+
lastError?: string;
|
|
80
|
+
access: "read" | "write";
|
|
81
|
+
};
|
|
82
|
+
/** One fire of a job, as the job's `lastRun` copy serves it. */
|
|
83
|
+
export type JobRunWire = {
|
|
84
|
+
jobId: string;
|
|
85
|
+
version: number;
|
|
86
|
+
fireAt: string;
|
|
87
|
+
manual: boolean;
|
|
88
|
+
conversationId?: number;
|
|
89
|
+
taskId?: number;
|
|
90
|
+
outcome: "running" | "succeeded" | "failed" | "skipped";
|
|
91
|
+
startedAt: string;
|
|
92
|
+
settledAt?: string;
|
|
93
|
+
error?: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* One immutable revision of a job (`sigrun.job.revision`, keyed
|
|
97
|
+
* `<jobId>:<version>`): the spec exactly as it stood at that version.
|
|
98
|
+
*/
|
|
99
|
+
export type JobRevisionWire = {
|
|
100
|
+
jobId: string;
|
|
101
|
+
version: number;
|
|
102
|
+
source: "api" | "file" | "extension";
|
|
103
|
+
spec: JobSpec;
|
|
104
|
+
owner?: string;
|
|
105
|
+
sharing?: Sharing;
|
|
106
|
+
enabled: boolean;
|
|
107
|
+
changedAt: string;
|
|
108
|
+
changedBy: string;
|
|
109
|
+
};
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §8.2 job wire types — THE jobs channel contract.
|
|
3
|
+
*
|
|
4
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server owns
|
|
5
|
+
* the runtime half (src/jobs/* — validation, schedule math) and both
|
|
6
|
+
* clients (webui, tui) import these types directly. Like every wire
|
|
7
|
+
* module, imports are TYPE-ONLY from sibling shared modules (the
|
|
8
|
+
* views-purity guard enforces this), so the module erases at compile
|
|
9
|
+
* time and contributes no runtime.
|
|
10
|
+
*
|
|
11
|
+
* The shapes follow spec §8.2's schedule/action grammar verbatim:
|
|
12
|
+
* a schedule is exactly one of a 5-field `cron` (with an optional IANA
|
|
13
|
+
* `timezone`, defaulting to the server timezone), an `every` interval
|
|
14
|
+
* (`^\\d+(s|m|h|d)$`, minimum 60 s), or a one-shot ISO 8601 `at`; an
|
|
15
|
+
* action is a `prompt` (optionally pinning the model/thinking of a NEW
|
|
16
|
+
* conversation, or targeting a fixed conversation id) or a `task`
|
|
17
|
+
* (a registered extension task name plus optional JSON input).
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON value mirrors (the shared leaf's local declarations of
|
|
3
|
+
* chord/pi-durable's `JsonValue`/`JsonObject`).
|
|
4
|
+
*
|
|
5
|
+
* Declared LOCALLY — never imported — so every shared module stays
|
|
6
|
+
* import-free at runtime (the shared-package invariant: the webui
|
|
7
|
+
* compiles DOM-only against these files, and a dependency import would
|
|
8
|
+
* drag a module only one client can resolve). The shapes mirror
|
|
9
|
+
* `@earendil-works/chord`'s `JsonValue` exactly (structural identity —
|
|
10
|
+
* the server-side doc definitions accept records built from these types
|
|
11
|
+
* wherever pi-durable's own `JsonObject` is required).
|
|
12
|
+
*/
|
|
13
|
+
/** A strict-JSON value: primitives, arrays, or a JSON object. */
|
|
14
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
15
|
+
[key: string]: JsonValue;
|
|
16
|
+
};
|
|
17
|
+
/** JSON object used as the root of every durable document. */
|
|
18
|
+
export type JsonObject = {
|
|
19
|
+
[key: string]: JsonValue;
|
|
20
|
+
};
|
package/dist/json.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON value mirrors (the shared leaf's local declarations of
|
|
3
|
+
* chord/pi-durable's `JsonValue`/`JsonObject`).
|
|
4
|
+
*
|
|
5
|
+
* Declared LOCALLY — never imported — so every shared module stays
|
|
6
|
+
* import-free at runtime (the shared-package invariant: the webui
|
|
7
|
+
* compiles DOM-only against these files, and a dependency import would
|
|
8
|
+
* drag a module only one client can resolve). The shapes mirror
|
|
9
|
+
* `@earendil-works/chord`'s `JsonValue` exactly (structural identity —
|
|
10
|
+
* the server-side doc definitions accept records built from these types
|
|
11
|
+
* wherever pi-durable's own `JsonObject` is required).
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `docs["pi.live"]` LiveState narrowing (gateway plan 1, Task 1).
|
|
3
|
+
*
|
|
4
|
+
* `pi.live` is the harness's live-run document — `run` while a generation
|
|
5
|
+
* is in flight, `generation.message` (the committed throttled partial of
|
|
6
|
+
* the response), `tools` (live tool slots), `compactions`, and
|
|
7
|
+
* `compactionFailure` (shape per pi-durable dist/harness/live.d.ts).
|
|
8
|
+
* `ConversationViewBase` types `docs` values as `JsonObject`: named
|
|
9
|
+
* LiveDocs are narrowed by their consumers, and this module is that
|
|
10
|
+
* narrowing for `pi.live` — shared by the webui, the tui and the gateway
|
|
11
|
+
* engine so every reader narrows the doc identically.
|
|
12
|
+
*
|
|
13
|
+
* Purity: a shared wire module — `import type` sibling-only (the purity
|
|
14
|
+
* guard in test/views-purity.test.ts); the runtime functions below are
|
|
15
|
+
* dependency-free so both clients (DOM-only webui, NodeNext tui) and the
|
|
16
|
+
* server read the same doc the same way.
|
|
17
|
+
*/
|
|
18
|
+
import type { ConversationViewBase } from "./views.js";
|
|
19
|
+
/** One live tool slot of `docs["pi.live"]` (the LiveState tools list). */
|
|
20
|
+
export interface LiveToolSlotWire {
|
|
21
|
+
readonly callId: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly status: string;
|
|
24
|
+
readonly output?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The `docs["pi.live"]` LiveState doc, narrowed to what the channels read:
|
|
28
|
+
* `run` (present exactly while busy — the steer field derives from it
|
|
29
|
+
* server-side), `generation.message` (the committed throttled partial of
|
|
30
|
+
* the in-flight response), `tools` (live tool slots), `compactions` (live
|
|
31
|
+
* compaction tasks), `compactionFailure` (the newest failure — a
|
|
32
|
+
* completed compaction clears it; nothing-to-compact or a fault leaves
|
|
33
|
+
* it).
|
|
34
|
+
*/
|
|
35
|
+
export interface LiveDocWire {
|
|
36
|
+
readonly run?: unknown;
|
|
37
|
+
readonly generation?: {
|
|
38
|
+
readonly message?: unknown;
|
|
39
|
+
};
|
|
40
|
+
readonly tools?: readonly LiveToolSlotWire[];
|
|
41
|
+
readonly compactions?: readonly unknown[];
|
|
42
|
+
readonly compactionFailure?: {
|
|
43
|
+
readonly error?: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The view's `pi.live` doc, or undefined when absent (idle and never-run
|
|
48
|
+
* conversations carry no doc). A cast, not a validation: the shape is
|
|
49
|
+
* pinned by the harness writer and the fail-soft reads below absorb
|
|
50
|
+
* drift.
|
|
51
|
+
*/
|
|
52
|
+
export declare function liveDoc(view: ConversationViewBase): LiveDocWire | undefined;
|
|
53
|
+
/** The live tool slots of the view's `pi.live` doc (empty when absent). */
|
|
54
|
+
export declare function liveToolSlots(view: ConversationViewBase): readonly LiveToolSlotWire[];
|
|
55
|
+
/**
|
|
56
|
+
* The in-flight generation's committed partial message, or undefined when
|
|
57
|
+
* idle (or the doc shape is unexpected — fail soft to absent; upstream
|
|
58
|
+
* drift surfaces in the schema fixture first).
|
|
59
|
+
*/
|
|
60
|
+
export declare function liveGenerationPartial(view: ConversationViewBase): object | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The in-flight generation's visible text: the partial's text blocks
|
|
63
|
+
* (`{type: "text"}`) joined with `""` — thinking and tool-call blocks
|
|
64
|
+
* contribute nothing. `""` when idle or the content is not an array; a
|
|
65
|
+
* string `content` returns as-is.
|
|
66
|
+
*/
|
|
67
|
+
export declare function liveGenerationText(view: ConversationViewBase): string;
|
|
68
|
+
/** How many compaction tasks are live right now (0 = none). */
|
|
69
|
+
export declare function liveCompactions(view: ConversationViewBase): number;
|
|
70
|
+
/** The newest compaction failure's error text, when one is surfaced. */
|
|
71
|
+
export declare function liveCompactionFailure(view: ConversationViewBase): string | undefined;
|
package/dist/live.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `docs["pi.live"]` LiveState narrowing (gateway plan 1, Task 1).
|
|
3
|
+
*
|
|
4
|
+
* `pi.live` is the harness's live-run document — `run` while a generation
|
|
5
|
+
* is in flight, `generation.message` (the committed throttled partial of
|
|
6
|
+
* the response), `tools` (live tool slots), `compactions`, and
|
|
7
|
+
* `compactionFailure` (shape per pi-durable dist/harness/live.d.ts).
|
|
8
|
+
* `ConversationViewBase` types `docs` values as `JsonObject`: named
|
|
9
|
+
* LiveDocs are narrowed by their consumers, and this module is that
|
|
10
|
+
* narrowing for `pi.live` — shared by the webui, the tui and the gateway
|
|
11
|
+
* engine so every reader narrows the doc identically.
|
|
12
|
+
*
|
|
13
|
+
* Purity: a shared wire module — `import type` sibling-only (the purity
|
|
14
|
+
* guard in test/views-purity.test.ts); the runtime functions below are
|
|
15
|
+
* dependency-free so both clients (DOM-only webui, NodeNext tui) and the
|
|
16
|
+
* server read the same doc the same way.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The view's `pi.live` doc, or undefined when absent (idle and never-run
|
|
20
|
+
* conversations carry no doc). A cast, not a validation: the shape is
|
|
21
|
+
* pinned by the harness writer and the fail-soft reads below absorb
|
|
22
|
+
* drift.
|
|
23
|
+
*/
|
|
24
|
+
export function liveDoc(view) {
|
|
25
|
+
// SAFETY: `docs` values are `JsonObject` by wire contract; `pi.live` is
|
|
26
|
+
// the harness's own LiveState document. The cast narrows the wire type,
|
|
27
|
+
// it does not validate — every reader below is fail-soft against drift.
|
|
28
|
+
return view.docs["pi.live"];
|
|
29
|
+
}
|
|
30
|
+
/** The live tool slots of the view's `pi.live` doc (empty when absent). */
|
|
31
|
+
export function liveToolSlots(view) {
|
|
32
|
+
return liveDoc(view)?.tools ?? [];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The in-flight generation's committed partial message, or undefined when
|
|
36
|
+
* idle (or the doc shape is unexpected — fail soft to absent; upstream
|
|
37
|
+
* drift surfaces in the schema fixture first).
|
|
38
|
+
*/
|
|
39
|
+
export function liveGenerationPartial(view) {
|
|
40
|
+
const message = liveDoc(view)?.generation?.message;
|
|
41
|
+
if (typeof message !== "object" || message === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The in-flight generation's visible text: the partial's text blocks
|
|
47
|
+
* (`{type: "text"}`) joined with `""` — thinking and tool-call blocks
|
|
48
|
+
* contribute nothing. `""` when idle or the content is not an array; a
|
|
49
|
+
* string `content` returns as-is.
|
|
50
|
+
*/
|
|
51
|
+
export function liveGenerationText(view) {
|
|
52
|
+
const partial = liveGenerationPartial(view);
|
|
53
|
+
if (partial === undefined)
|
|
54
|
+
return "";
|
|
55
|
+
const content = partial.content;
|
|
56
|
+
if (typeof content === "string")
|
|
57
|
+
return content;
|
|
58
|
+
if (!Array.isArray(content))
|
|
59
|
+
return "";
|
|
60
|
+
let text = "";
|
|
61
|
+
for (const block of content) {
|
|
62
|
+
if (typeof block !== "object" || block === null)
|
|
63
|
+
continue;
|
|
64
|
+
const typed = block;
|
|
65
|
+
if (typed.type !== "text" || typeof typed.text !== "string")
|
|
66
|
+
continue;
|
|
67
|
+
text += typed.text;
|
|
68
|
+
}
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
/** How many compaction tasks are live right now (0 = none). */
|
|
72
|
+
export function liveCompactions(view) {
|
|
73
|
+
return liveDoc(view)?.compactions?.length ?? 0;
|
|
74
|
+
}
|
|
75
|
+
/** The newest compaction failure's error text, when one is surfaced. */
|
|
76
|
+
export function liveCompactionFailure(view) {
|
|
77
|
+
const failure = liveDoc(view)?.compactionFailure;
|
|
78
|
+
if (typeof failure !== "object" || failure === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
const error = failure.error;
|
|
81
|
+
return typeof error === "string" && error !== "" ? error : undefined;
|
|
82
|
+
}
|