@sigrun/sigrun 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 +214 -0
- package/CONTEXT.md +163 -0
- package/LICENSE +21 -0
- package/README.md +691 -2
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/backup/marker.js +80 -0
- package/dist/backup/run-backup.js +339 -0
- package/dist/cli.js +952 -0
- package/dist/commands/expand.js +245 -0
- package/dist/commands/templates.js +272 -0
- package/dist/config.js +1146 -0
- package/dist/extension.d.ts +431 -0
- package/dist/harness/activity.js +42 -0
- package/dist/harness/ambient.js +261 -0
- package/dist/harness/auth.js +130 -0
- package/dist/harness/boot.js +865 -0
- package/dist/harness/builtins.js +191 -0
- package/dist/harness/event-bus.js +1203 -0
- package/dist/harness/health.js +30 -0
- package/dist/harness/identity/index.js +37 -0
- package/dist/harness/identity/invites.js +94 -0
- package/dist/harness/identity/json-store.js +108 -0
- package/dist/harness/identity/setup-code.js +81 -0
- package/dist/harness/identity/tokens.js +101 -0
- package/dist/harness/identity/users.js +93 -0
- package/dist/harness/interactions.js +283 -0
- package/dist/harness/loader.js +1236 -0
- package/dist/harness/memory-default.js +35 -0
- package/dist/harness/naming.js +193 -0
- package/dist/harness/packages.js +1002 -0
- package/dist/harness/providers/auth-bridge.js +60 -0
- package/dist/harness/providers/index.js +277 -0
- package/dist/harness/providers/models-store.js +105 -0
- package/dist/harness/providers/refresh.js +161 -0
- package/dist/harness/providers/scheduler.js +89 -0
- package/dist/harness/registry.js +9 -0
- package/dist/harness/reserved-sections.js +24 -0
- package/dist/harness/seam-host.js +239 -0
- package/dist/harness/sections/context-files.js +78 -0
- package/dist/harness/sections/model-guidance.js +77 -0
- package/dist/harness/settings.js +1053 -0
- package/dist/harness/stores.js +450 -0
- package/dist/harness/tools.js +500 -0
- package/dist/harness/workspace.js +73 -0
- package/dist/index.js +39 -0
- package/dist/jobs/docs.js +257 -0
- package/dist/jobs/fire.js +342 -0
- package/dist/jobs/index.js +228 -0
- package/dist/jobs/observe.js +165 -0
- package/dist/jobs/reconcile.js +245 -0
- package/dist/jobs/schedule.js +77 -0
- package/dist/jobs/spec.js +248 -0
- package/dist/jobs/task.js +190 -0
- package/dist/jobs/transfer.js +53 -0
- package/dist/serve.js +164 -0
- package/dist/server/auth/claim-lock.js +28 -0
- package/dist/server/auth/ip.js +51 -0
- package/dist/server/auth/login-limiter.js +52 -0
- package/dist/server/auth/loopback.js +47 -0
- package/dist/server/auth/pipeline.js +330 -0
- package/dist/server/auth/principal.js +26 -0
- package/dist/server/commands.js +198 -0
- package/dist/server/content-parts.js +142 -0
- package/dist/server/context.js +13 -0
- package/dist/server/conversation-handler.js +61 -0
- package/dist/server/conversation-ops.js +310 -0
- package/dist/server/events/hub.js +39 -0
- package/dist/server/events/types.js +8 -0
- package/dist/server/helpers.js +351 -0
- package/dist/server/host-bridge.js +131 -0
- package/dist/server/index.js +232 -0
- package/dist/server/notifications.js +185 -0
- package/dist/server/ownership.js +67 -0
- package/dist/server/readiness.js +36 -0
- package/dist/server/routes/auth.js +172 -0
- package/dist/server/routes/conversations.js +341 -0
- package/dist/server/routes/events.js +161 -0
- package/dist/server/routes/extension-routes.js +127 -0
- package/dist/server/routes/extensions.js +168 -0
- package/dist/server/routes/files.js +308 -0
- package/dist/server/routes/interactions.js +260 -0
- package/dist/server/routes/jobs.js +271 -0
- package/dist/server/routes/meta.js +146 -0
- package/dist/server/routes/metrics.js +219 -0
- package/dist/server/routes/onboarding.js +268 -0
- package/dist/server/routes/probes.js +53 -0
- package/dist/server/routes/providers.js +198 -0
- package/dist/server/routes/settings.js +48 -0
- package/dist/server/routes/stream.js +131 -0
- package/dist/server/routes/submissions.js +317 -0
- package/dist/server/routes/users.js +169 -0
- package/dist/server/schema-fixture.js +950 -0
- package/dist/server/session-meta.js +247 -0
- package/dist/server/static.js +169 -0
- package/dist/server/views.js +71 -0
- package/dist/skills/index.js +418 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
- package/node_modules/@sigrun/shared/LICENSE +21 -0
- package/node_modules/@sigrun/shared/README.md +10 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
- package/node_modules/@sigrun/shared/dist/access.js +72 -0
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
- package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
- package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
- package/node_modules/@sigrun/shared/dist/commands.js +25 -0
- package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
- package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
- package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
- package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
- package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
- package/node_modules/@sigrun/shared/dist/json.js +13 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
- package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
- package/node_modules/@sigrun/shared/dist/settings.js +19 -0
- package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
- package/node_modules/@sigrun/shared/dist/themes.js +156 -0
- package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
- package/node_modules/@sigrun/shared/dist/turns.js +166 -0
- package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
- package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
- package/node_modules/@sigrun/shared/dist/views.js +38 -0
- package/node_modules/@sigrun/shared/package.json +83 -0
- package/package.json +89 -5
- package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
- package/webui/dist/assets/index-rOiARZLF.js +50 -0
- package/webui/dist/index.html +37 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The documented extension contract (core hardening W5 Task 1, spec
|
|
3
|
+
* §9.1): the ONE type surface an extension package programs against —
|
|
4
|
+
* `@sigrun/sigrun/extension`, a TYPES-ONLY subpath of the main package (the
|
|
5
|
+
* 2026-10-13 types-in-main-package ruling; there is no separate
|
|
6
|
+
* contract package).
|
|
7
|
+
*
|
|
8
|
+
* Authors consume it with `import type` only:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import type { SigrunExtensionResult, SigrunHost } from "@sigrun/sigrun/extension";
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The subpath has NO runtime entry: nothing loads when an extension is
|
|
15
|
+
* imported, and importing it at runtime fails loud by design. See
|
|
16
|
+
* docs/extending.md ("The factory result") for the field table and the
|
|
17
|
+
* host rule.
|
|
18
|
+
*
|
|
19
|
+
* Module discipline (load-bearing, pinned by test/extension-contract.test.ts):
|
|
20
|
+
*
|
|
21
|
+
* - TYPES-ONLY and self-contained — no runtime code, no re-exported
|
|
22
|
+
* values. The emitted `dist/extension.d.ts` must stand alone.
|
|
23
|
+
* - `import type` from EXACTLY three roots: `@earendil-works/pi-durable`,
|
|
24
|
+
* `@earendil-works/pi-ai`, and `@sigrun/shared/*`. These are the
|
|
25
|
+
* packages an extension declares as devDependencies (the loader links
|
|
26
|
+
* the first two's host copies into the packages dir); anything else
|
|
27
|
+
* would leak a new dependency onto every contract consumer. Notably
|
|
28
|
+
* the invocation `Context` is NOT imported from
|
|
29
|
+
* `@earendil-works/chord` — the contract spells its own structural
|
|
30
|
+
* {@link HandlerContext} instead.
|
|
31
|
+
* - The loader (src/harness/loader.ts) references these types for its
|
|
32
|
+
* validated candidate shape and a compile-time key assertion, so the
|
|
33
|
+
* implementation and this contract cannot drift silently.
|
|
34
|
+
*/
|
|
35
|
+
import type { AssistantMessage, Provider } from "@earendil-works/pi-ai";
|
|
36
|
+
import type { ConversationDocToken, ConversationId, Cursor, EntryId, EntryQuery, EntryRecord, Extension, InputSubmissionDraft, JsonObject, ModelRef, Page, SubmissionId } from "@earendil-works/pi-durable";
|
|
37
|
+
import type { Access, Sharing } from "@sigrun/shared/access";
|
|
38
|
+
import type { AmbientBody, AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
|
|
39
|
+
import type { InteractionAnswer, InteractionRecord } from "@sigrun/shared/interactions";
|
|
40
|
+
import type { JobSpec } from "@sigrun/shared/jobs";
|
|
41
|
+
import type { ConversationSummary, ConversationViewWire, HealthStatus, SessionMeta, SubmissionRecordWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
|
|
42
|
+
export type { Access, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, Sharing, };
|
|
43
|
+
/**
|
|
44
|
+
* The invocation-scoped context handlers and host APIs receive: a
|
|
45
|
+
* structural view of the runtime context (its cancellation signal).
|
|
46
|
+
* Declared structurally — not imported from `@earendil-works/chord` —
|
|
47
|
+
* so this module stays inside the contract allowlist; the value the
|
|
48
|
+
* host passes is the full runtime context, which satisfies this shape.
|
|
49
|
+
*/
|
|
50
|
+
export interface HandlerContext {
|
|
51
|
+
/** Aborts with the caller's cancellation; observe it in long work. */
|
|
52
|
+
readonly abortSignal: AbortSignal | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The extension-facing settled-run event: one per settled run, with the
|
|
56
|
+
* run's input texts host-resolved from the durable transcript.
|
|
57
|
+
* `inputTexts` is position-paired with `inputs` — an input whose entry
|
|
58
|
+
* is unresolvable (withdrawn while queued, withdrawn entry, read
|
|
59
|
+
* failure) contributes `""` at its position, never a reorder.
|
|
60
|
+
*/
|
|
61
|
+
export type RunSettledEvent = {
|
|
62
|
+
readonly conversationId: ConversationId;
|
|
63
|
+
readonly inputs: readonly SubmissionId[];
|
|
64
|
+
/** Text blocks of each input's entry (`pi.user`, or an expanded input's
|
|
65
|
+
* `sigrun.user.expanded` — the ADMITTED text), concatenated;
|
|
66
|
+
* unresolvable/withdrawn → "" at that position. */
|
|
67
|
+
readonly inputTexts: readonly string[];
|
|
68
|
+
readonly outcome: {
|
|
69
|
+
readonly status: "answered";
|
|
70
|
+
readonly answerEntryId: EntryId;
|
|
71
|
+
readonly answer: AssistantMessage;
|
|
72
|
+
} | {
|
|
73
|
+
readonly status: "unanswered";
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The narrowed read surface a `runSettled` handler receives: exactly
|
|
79
|
+
* `entry` / `entries` / `document` over the settled conversation — no
|
|
80
|
+
* `taskId`, no `memo`, no `conversationId` (the event carries those).
|
|
81
|
+
* `entries` mirrors the conversation's entry history (the conversation
|
|
82
|
+
* is implicit); `document` is the committed conversation-doc read.
|
|
83
|
+
*/
|
|
84
|
+
export type MemoryEventApi = {
|
|
85
|
+
/** One visible entry of the settled conversation by exact id (`undefined` when absent). */
|
|
86
|
+
entry(id: EntryId, context: HandlerContext): Promise<EntryRecord | undefined>;
|
|
87
|
+
/** Fork-aware entry history of the settled conversation. */
|
|
88
|
+
entries(query: Omit<EntryQuery, "conversationId">, limit: number, cursor: Cursor | undefined, context: HandlerContext): Promise<Page<EntryRecord, Cursor>>;
|
|
89
|
+
/** Committed conversation-document read. */
|
|
90
|
+
document<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: HandlerContext): Promise<Readonly<T> | undefined>;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The extension-facing payloads of the lifecycle events: the handler
|
|
94
|
+
* NAME discriminates (like pi-durable hooks), so the payloads carry no
|
|
95
|
+
* `type` field.
|
|
96
|
+
*/
|
|
97
|
+
export type ExtensionLifecyclePayload = {
|
|
98
|
+
conversationCreated: {
|
|
99
|
+
readonly conversationId: ConversationId;
|
|
100
|
+
};
|
|
101
|
+
conversationForked: {
|
|
102
|
+
readonly childId: ConversationId;
|
|
103
|
+
readonly parentId: ConversationId;
|
|
104
|
+
readonly at: EntryId;
|
|
105
|
+
};
|
|
106
|
+
conversationArchived: {
|
|
107
|
+
readonly conversationId: ConversationId;
|
|
108
|
+
readonly archived: boolean;
|
|
109
|
+
};
|
|
110
|
+
conversationIdle: {
|
|
111
|
+
readonly conversationId: ConversationId;
|
|
112
|
+
readonly idleMs: number;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
/** A factory-result `runSettled` handler: called once per settled run, deadline-bounded. */
|
|
116
|
+
export type ExtensionRunSettledHandler = (event: RunSettledEvent, api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
|
|
117
|
+
/** A factory-result lifecycle handler for one event, deadline-bounded. */
|
|
118
|
+
export type ExtensionLifecycleHandler<K extends keyof ExtensionLifecyclePayload> = (event: ExtensionLifecyclePayload[K], api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
|
|
119
|
+
/** A factory-result `harnessShutdown` handler (no event payload — shutdown is conversation-less). */
|
|
120
|
+
export type ExtensionHarnessShutdownHandler = (api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* The `userDeleted` payload (core hardening W1): which directory user
|
|
123
|
+
* left and who received their resources. Emitted after the ownership
|
|
124
|
+
* transfer, token revocation, and record removal have landed.
|
|
125
|
+
*/
|
|
126
|
+
export interface UserDeletedPayload {
|
|
127
|
+
readonly user: string;
|
|
128
|
+
readonly transferTo: string;
|
|
129
|
+
}
|
|
130
|
+
/** A factory-result `userDeleted` handler (the transfer counterpart of harnessShutdown). */
|
|
131
|
+
export type ExtensionUserDeletedHandler = (event: UserDeletedPayload, api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
|
|
132
|
+
/** The lifecycle handler fields of the factory result, one per event. */
|
|
133
|
+
export type SigrunLifecycleHandlers = {
|
|
134
|
+
readonly conversationCreated?: ExtensionLifecycleHandler<"conversationCreated">;
|
|
135
|
+
readonly conversationForked?: ExtensionLifecycleHandler<"conversationForked">;
|
|
136
|
+
readonly conversationArchived?: ExtensionLifecycleHandler<"conversationArchived">;
|
|
137
|
+
readonly conversationIdle?: ExtensionLifecycleHandler<"conversationIdle">;
|
|
138
|
+
readonly harnessShutdown?: ExtensionHarnessShutdownHandler;
|
|
139
|
+
/** The user-removal transfer event; direct fan-out, not commit-derived. */
|
|
140
|
+
readonly userDeleted?: ExtensionUserDeletedHandler;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The lifecycle event names a worker's `wakeOn` may declare.
|
|
144
|
+
* `harnessShutdown` is excluded — workers stop BEFORE the shutdown
|
|
145
|
+
* handlers run (the shutdown ordering), so waking on it is meaningless.
|
|
146
|
+
*/
|
|
147
|
+
export type WorkerWakeEvent = keyof ExtensionLifecyclePayload;
|
|
148
|
+
/** The wake argument of an event-triggered worker run. */
|
|
149
|
+
export type WorkerWake = {
|
|
150
|
+
readonly event: string;
|
|
151
|
+
readonly conversationId: ConversationId;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* One factory-declared idle worker (the curator contract). Host-owned
|
|
155
|
+
* lifecycle: sigrun taps the lifecycle fan-out for `wakeOn` matches.
|
|
156
|
+
* One run at a time per worker; a wake during an in-flight run is
|
|
157
|
+
* queued + coalesced (one pending-wake slot, last wins) and delivered
|
|
158
|
+
* as the next run's `wake`.
|
|
159
|
+
*
|
|
160
|
+
* `every` is RETIRED (ruling 12, 2026-10-19): this type has NO `every`
|
|
161
|
+
* field — jobs are the only time-based scheduler; an extension that
|
|
162
|
+
* needs a timer declares a job with a `task` action.
|
|
163
|
+
*/
|
|
164
|
+
export interface ExtensionWorker {
|
|
165
|
+
/** Unique within the extension; surfaces in /v1/meta. */
|
|
166
|
+
readonly name: string;
|
|
167
|
+
/** Lifecycle events that wake the worker (REQUIRED, non-empty). */
|
|
168
|
+
readonly wakeOn: readonly WorkerWakeEvent[];
|
|
169
|
+
/** The work; rejects (or overruns the worker deadline) to fail the run. */
|
|
170
|
+
run(context: HandlerContext, wake?: WorkerWake): Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
/** Three-state per-provider readiness: not configured, degraded, or ready. */
|
|
173
|
+
export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
|
|
174
|
+
/** One readiness report: the status plus optional operator-facing detail. */
|
|
175
|
+
export interface ProviderReadiness {
|
|
176
|
+
readonly status: ProviderReadinessStatus;
|
|
177
|
+
readonly detail?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A plugin's per-provider readiness tracker: a synchronous per-request
|
|
181
|
+
* read over plugin-owned state (never network, never storage) — the
|
|
182
|
+
* three-state source `/v1/auth/check` reports verbatim.
|
|
183
|
+
*/
|
|
184
|
+
export type ReadinessTracker = () => ProviderReadiness;
|
|
185
|
+
/** One provider contribution: a pi-ai `createProvider()` result plus optional boot hints. */
|
|
186
|
+
export interface ProviderContribution {
|
|
187
|
+
readonly provider: Provider;
|
|
188
|
+
/**
|
|
189
|
+
* This contributor's proposed default model. Ignored when a default
|
|
190
|
+
* model is configured; the last contributor to propose one wins
|
|
191
|
+
* otherwise.
|
|
192
|
+
*/
|
|
193
|
+
readonly defaultModel?: ModelRef;
|
|
194
|
+
/** The per-provider readiness tracker (`/v1/auth/check` reports it verbatim). */
|
|
195
|
+
readonly readiness?: ReadinessTracker;
|
|
196
|
+
}
|
|
197
|
+
/** One contributed webui bundle: a selection name plus the bundle root directory. */
|
|
198
|
+
export interface WebuiBundleContribution {
|
|
199
|
+
/** The selection key (`server.webui.bundle`); must match `^[a-z][a-z0-9_-]*$`. */
|
|
200
|
+
readonly name: string;
|
|
201
|
+
/** The bundle root (the directory containing `index.html`); relative values anchor at the extension ENTRY's directory. */
|
|
202
|
+
readonly dir: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The resolved caller (core hardening W1): what the auth pipeline
|
|
206
|
+
* stamps on every request. `method` names the proof — the empty-method
|
|
207
|
+
* naive path (`"none"`), a verified token (`"token"`), or a trusted
|
|
208
|
+
* forward-auth header set (`"proxy"`).
|
|
209
|
+
*/
|
|
210
|
+
export interface Principal {
|
|
211
|
+
/** The directory username; `undefined` under the naive method with no header. */
|
|
212
|
+
readonly user: string | undefined;
|
|
213
|
+
readonly name?: string;
|
|
214
|
+
readonly email?: string;
|
|
215
|
+
readonly admin: boolean;
|
|
216
|
+
/** True for the `SIGRUN_API_TOKEN` service principal. */
|
|
217
|
+
readonly service: boolean;
|
|
218
|
+
readonly method: "none" | "token" | "proxy";
|
|
219
|
+
}
|
|
220
|
+
/** One extension HTTP route, mounted at `/v1/ext/<slug>/<path>`. */
|
|
221
|
+
export interface ExtensionRouteContribution {
|
|
222
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
223
|
+
/** Relative; mounted at `/v1/ext/<slug>/<path>`. */
|
|
224
|
+
readonly path: string;
|
|
225
|
+
readonly auth: "user" | "admin" | "public";
|
|
226
|
+
/** Request body ceiling in bytes (default 1 MiB). */
|
|
227
|
+
readonly bodyLimit?: number;
|
|
228
|
+
/** Web-standard `Request`/`Response` (Node ≥ 22) — no server type crosses the contract. */
|
|
229
|
+
handler(request: Request, ctx: {
|
|
230
|
+
readonly principal?: Principal;
|
|
231
|
+
readonly host: SigrunHost;
|
|
232
|
+
}): Promise<Response>;
|
|
233
|
+
}
|
|
234
|
+
/** One extension slash command, invoked via `POST /v1/conversations/:id/commands/:name`. */
|
|
235
|
+
export interface ExtensionCommandContribution {
|
|
236
|
+
/** Must match `^[a-z][a-z0-9-]*$`; core command names are reserved. */
|
|
237
|
+
readonly name: string;
|
|
238
|
+
readonly description: string;
|
|
239
|
+
/** Argument hint for the composer's `/` menu. */
|
|
240
|
+
readonly args?: string;
|
|
241
|
+
run(ctx: {
|
|
242
|
+
readonly conversationId: ConversationId;
|
|
243
|
+
readonly args: string;
|
|
244
|
+
readonly principal: Principal;
|
|
245
|
+
readonly host: SigrunHost;
|
|
246
|
+
}): Promise<{
|
|
247
|
+
readonly notice?: string;
|
|
248
|
+
} | void>;
|
|
249
|
+
}
|
|
250
|
+
/** One notification core delivers to every registered notifier (best-effort). */
|
|
251
|
+
export interface NotificationEvent {
|
|
252
|
+
readonly type: "interaction.pending" | "job.failed" | "run.settled";
|
|
253
|
+
readonly conversationId?: number;
|
|
254
|
+
readonly jobId?: string;
|
|
255
|
+
/** The recipient. */
|
|
256
|
+
readonly user: string | undefined;
|
|
257
|
+
readonly title: string;
|
|
258
|
+
readonly body: string;
|
|
259
|
+
/** `${server.publicUrl}/#/c/<conversationId>` when `server.publicUrl` is set; absent otherwise. */
|
|
260
|
+
readonly url?: string;
|
|
261
|
+
}
|
|
262
|
+
/** One notifier transport (ntfy, Signal, web push — core ships none). */
|
|
263
|
+
export interface NotifierContribution {
|
|
264
|
+
readonly name: string;
|
|
265
|
+
notify(event: NotificationEvent, ctx: {
|
|
266
|
+
readonly host: SigrunHost;
|
|
267
|
+
}): Promise<void>;
|
|
268
|
+
}
|
|
269
|
+
/** One wire part of a `host.conversations.submit` input array. */
|
|
270
|
+
export type SubmissionInputPart = {
|
|
271
|
+
readonly type: "text";
|
|
272
|
+
readonly text: string;
|
|
273
|
+
} | {
|
|
274
|
+
readonly type: "image";
|
|
275
|
+
readonly path: string;
|
|
276
|
+
readonly mime?: string;
|
|
277
|
+
} | {
|
|
278
|
+
readonly type: "file";
|
|
279
|
+
readonly path: string;
|
|
280
|
+
readonly mime?: string;
|
|
281
|
+
};
|
|
282
|
+
/** How a submit behaves when a run is already live (pi-durable's `whenBusy`). */
|
|
283
|
+
export type WhenBusyMode = NonNullable<InputSubmissionDraft["whenBusy"]>;
|
|
284
|
+
/**
|
|
285
|
+
* The host context sigrun hands an extension factory: the scoped
|
|
286
|
+
* extension stores, the interaction/ambient seam surfaces, and the
|
|
287
|
+
* conversation/identity/access APIs.
|
|
288
|
+
*
|
|
289
|
+
* The store surface is safe at any time (plain file I/O); the seam and
|
|
290
|
+
* conversation surfaces bind the harness lazily — call them from tool
|
|
291
|
+
* execute, hooks, routes, commands, workers, and jobs, never from a
|
|
292
|
+
* factory body.
|
|
293
|
+
*/
|
|
294
|
+
export interface SigrunHost {
|
|
295
|
+
/** The scoped extension-stores handle (your secrets/settings sections + raw data/cache/state paths). */
|
|
296
|
+
readonly stores: {
|
|
297
|
+
/** Your section of secrets.json. */
|
|
298
|
+
readonly secrets: {
|
|
299
|
+
get(key: string): Promise<string | undefined>;
|
|
300
|
+
set(key: string, value: string): Promise<void>;
|
|
301
|
+
};
|
|
302
|
+
/** Your settings/<slug>.json (absent reads as {}). */
|
|
303
|
+
readonly settings: {
|
|
304
|
+
get(): Promise<Record<string, unknown>>;
|
|
305
|
+
set(value: Record<string, unknown>): Promise<void>;
|
|
306
|
+
};
|
|
307
|
+
/** Your raw directories under the data/state/cache roots. */
|
|
308
|
+
readonly paths: {
|
|
309
|
+
readonly data: string;
|
|
310
|
+
readonly cache: string;
|
|
311
|
+
readonly state: string;
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
/** Seed (or fully replace) one interaction record; seeding a terminal record is a no-op. */
|
|
315
|
+
seedInteraction(conversationId: ConversationId, record: InteractionRecord, context: HandlerContext): Promise<void>;
|
|
316
|
+
/** Answer a pending interaction from the extension side (the tool path). */
|
|
317
|
+
answerInteraction(conversationId: ConversationId, callId: string, answer: InteractionAnswer, context: HandlerContext): Promise<boolean>;
|
|
318
|
+
/** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
|
|
319
|
+
settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
|
|
320
|
+
/** Read one interaction record (the poll loop's read). */
|
|
321
|
+
snapshotInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
|
|
322
|
+
/** Fully replace one ambient surface's state (write = replacement); the key must be unowned or yours. */
|
|
323
|
+
setAmbient(conversationId: ConversationId, record: {
|
|
324
|
+
readonly key: string;
|
|
325
|
+
readonly place: AmbientPlace;
|
|
326
|
+
readonly maxRows?: number;
|
|
327
|
+
readonly overflow?: "drop" | "wrap" | "summarize";
|
|
328
|
+
readonly refreshMs?: number;
|
|
329
|
+
readonly body?: AmbientBody;
|
|
330
|
+
readonly details?: JsonObject;
|
|
331
|
+
}, context: HandlerContext): Promise<void>;
|
|
332
|
+
/** Clear one ambient surface (empty replacement; keeps ownership). */
|
|
333
|
+
clearAmbient(conversationId: ConversationId, key: string, context: HandlerContext): Promise<void>;
|
|
334
|
+
/**
|
|
335
|
+
* Drive conversations in-process (channel bridges, jobs) over the
|
|
336
|
+
* same code paths as the HTTP API. Extensions are trusted in-process
|
|
337
|
+
* code: they may act for any user, and mapping a channel's users to
|
|
338
|
+
* sigrun users is the extension's own config.
|
|
339
|
+
*/
|
|
340
|
+
readonly conversations: {
|
|
341
|
+
create(opts: {
|
|
342
|
+
readonly owner?: string;
|
|
343
|
+
readonly sharing?: Sharing;
|
|
344
|
+
readonly name?: string;
|
|
345
|
+
readonly source?: string;
|
|
346
|
+
}): Promise<ConversationId>;
|
|
347
|
+
submit(id: ConversationId, input: readonly SubmissionInputPart[], opts?: {
|
|
348
|
+
readonly user?: string;
|
|
349
|
+
readonly requestId?: string;
|
|
350
|
+
readonly whenBusy?: WhenBusyMode;
|
|
351
|
+
}): Promise<SubmissionRecordWire>;
|
|
352
|
+
watch(id: ConversationId, listener: (view: ConversationViewWire) => void): Promise<() => Promise<void>>;
|
|
353
|
+
list(opts?: {
|
|
354
|
+
readonly owner?: string;
|
|
355
|
+
readonly archived?: boolean;
|
|
356
|
+
}): Promise<readonly ConversationSummary[]>;
|
|
357
|
+
meta(id: ConversationId): Promise<SessionMeta>;
|
|
358
|
+
};
|
|
359
|
+
/** The user directory (admin wire form; emails are admin material). */
|
|
360
|
+
readonly users: {
|
|
361
|
+
list(): Promise<readonly DirectoryUserWire[]>;
|
|
362
|
+
get(user: string): Promise<DirectoryUserWire | undefined>;
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Resolve one principal's access to one shared resource (the whole
|
|
366
|
+
* sharing contract for extension-owned records — core stores nothing
|
|
367
|
+
* for them).
|
|
368
|
+
*/
|
|
369
|
+
readonly access: {
|
|
370
|
+
accessOf(principal: Principal | {
|
|
371
|
+
readonly user: string;
|
|
372
|
+
}, resource: {
|
|
373
|
+
readonly owner?: string;
|
|
374
|
+
readonly sharing?: Sharing;
|
|
375
|
+
}): Promise<Access>;
|
|
376
|
+
};
|
|
377
|
+
/** The conversation's owner — who the agent acts for. */
|
|
378
|
+
readonly identity: {
|
|
379
|
+
actingUser(conversationId: ConversationId): Promise<string | undefined>;
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The documented factory-result contract: EVERY field an extension may
|
|
384
|
+
* return from its default-export factory. Every field is optional —
|
|
385
|
+
* a minimal extension returns only `extension`; a contributor-only
|
|
386
|
+
* result (`providers`/`webuiBundles` without `extension`) identifies
|
|
387
|
+
* itself by `name`.
|
|
388
|
+
*
|
|
389
|
+
* `routes`, `commands`, `skills`, and `notifiers` are declared here
|
|
390
|
+
* ahead of their implementations (W5 Tasks 2, 3, 4, and 6); the loader
|
|
391
|
+
* begins reading each when its task lands.
|
|
392
|
+
*/
|
|
393
|
+
export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
394
|
+
/** The pi-durable `Extension` to install; its declared name is the slug source. */
|
|
395
|
+
readonly extension?: Extension;
|
|
396
|
+
/** The contributor-only identity (required when contributing without an `extension`). */
|
|
397
|
+
readonly name?: string;
|
|
398
|
+
/** Provider contributions, merged into the boot model catalog (last-registered-wins, collisions recorded). */
|
|
399
|
+
readonly providers?: readonly ProviderContribution[];
|
|
400
|
+
/** Webui-bundle contributions; each `dir` anchors at the extension ENTRY's directory. */
|
|
401
|
+
readonly webuiBundles?: readonly WebuiBundleContribution[];
|
|
402
|
+
/** The surface's health producer, aggregated into `/v1/meta` `health` (any `"degraded"` degrades). */
|
|
403
|
+
readonly health?: () => HealthStatus;
|
|
404
|
+
/** Called once per settled run with the host-resolved input texts; deadline-bounded. */
|
|
405
|
+
readonly runSettled?: ExtensionRunSettledHandler;
|
|
406
|
+
/** Declared interaction kinds (the seam's write gate + capability inventory). */
|
|
407
|
+
readonly interactionKinds?: readonly string[];
|
|
408
|
+
/** Declared ambient surface keys (the seam's write gate + capability inventory). */
|
|
409
|
+
readonly ambientKinds?: readonly string[];
|
|
410
|
+
/** Idle workers (the curator contract — `wakeOn` only, `every` retired). */
|
|
411
|
+
readonly workers?: readonly ExtensionWorker[];
|
|
412
|
+
/** Declared jobs (the only time-based scheduler); read-only through the API under `ext:<slug>:<name>` ids. */
|
|
413
|
+
readonly jobs?: readonly JobSpec[];
|
|
414
|
+
/** HTTP routes mounted at `/v1/ext/<slug>/<path>` (lands in Task 2). */
|
|
415
|
+
readonly routes?: readonly ExtensionRouteContribution[];
|
|
416
|
+
/** Slash commands merged into `/v1/commands` (lands in Task 3). */
|
|
417
|
+
readonly commands?: readonly ExtensionCommandContribution[];
|
|
418
|
+
/** Absolute skill directories contributed by this extension (lands in Task 4). */
|
|
419
|
+
readonly skills?: readonly string[];
|
|
420
|
+
/** Notification transports (lands in Task 6). */
|
|
421
|
+
readonly notifiers?: readonly NotifierContribution[];
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The extension factory: sigrun calls it at load, passing the host
|
|
425
|
+
* context. THE HOST RULE: the first parameter is the host, never a
|
|
426
|
+
* defaulted options object — sigrun passes the host only when
|
|
427
|
+
* `factory.length > 0`, so a factory that wants the host must DECLARE
|
|
428
|
+
* the parameter (a length-0 factory may carry a defaulted first
|
|
429
|
+
* parameter that would consume the host as a real dependency).
|
|
430
|
+
*/
|
|
431
|
+
export type SigrunExtensionFactory = (host: SigrunHost) => SigrunExtensionResult | Promise<SigrunExtensionResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity-time stamping (UI refresh Task A1): the `lastActivityAt` seam.
|
|
3
|
+
*
|
|
4
|
+
* One server-owned session-meta field, stamped at exactly two moments —
|
|
5
|
+
* submission admission (the submissions route, so a sidebar sorts by
|
|
6
|
+
* activity the moment a prompt lands, before the run settles) and run
|
|
7
|
+
* settlement (the event bus's settled hook, so background runs — tasks,
|
|
8
|
+
* tools, other processes — stamp too). Both write through ONE
|
|
9
|
+
* `conversation.commit` setting `lastActivityAt = now().toISOString()`;
|
|
10
|
+
* the `now` seam is injectable so tests pin the instant.
|
|
11
|
+
*
|
|
12
|
+
* The stamp's only reader is the conversation listing
|
|
13
|
+
* (`ConversationSummary.lastActivityAt`) — never the wire view (the view
|
|
14
|
+
* mounts pi-durable's built-in docs only, so the schema fixture is
|
|
15
|
+
* unaffected by this write).
|
|
16
|
+
*/
|
|
17
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
18
|
+
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
19
|
+
/**
|
|
20
|
+
* Stamp `lastActivityAt` on one conversation's session-meta doc. A missing
|
|
21
|
+
* conversation is a no-op (the bus observes settles for conversations this
|
|
22
|
+
* process may no longer resolve — a dropped-elsewhere id must not fault
|
|
23
|
+
* the drain loop).
|
|
24
|
+
*/
|
|
25
|
+
export async function touchActivity(harness, conversationId, now, context) {
|
|
26
|
+
const conversation = await harness.conversation(conversationId, context);
|
|
27
|
+
if (conversation === undefined)
|
|
28
|
+
return;
|
|
29
|
+
await conversation.commit(async (tx) => {
|
|
30
|
+
const draft = await tx.doc(SessionMetaDoc, conversationId);
|
|
31
|
+
draft.lastActivityAt = now().toISOString();
|
|
32
|
+
}, context);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Wire the settle-side stamping: every settled run stamps its
|
|
36
|
+
* conversation. Registered right after `EventBus.open` (before extension
|
|
37
|
+
* registrations — host-owned machinery runs first); returns the
|
|
38
|
+
* unsubscribe (the bus's own close stops delivery regardless).
|
|
39
|
+
*/
|
|
40
|
+
export function wireActivity(bus, harness, now = () => new Date()) {
|
|
41
|
+
return bus.onSettled((group) => touchActivity(harness, group.conversationId, now, BACKGROUND_CONTEXT));
|
|
42
|
+
}
|