@sigrun/sigrun 0.8.0 → 0.10.0
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 +55 -87
- package/dist/config.js +47 -0
- package/dist/curation/host.js +125 -21
- package/dist/curation/runs.js +15 -4
- package/dist/extension.d.ts +268 -3
- package/dist/harness/boot.js +113 -3
- package/dist/harness/complete.js +127 -0
- package/dist/harness/decision-tool.js +93 -0
- package/dist/harness/decisions.js +477 -0
- package/dist/harness/event-bus.js +12 -0
- package/dist/harness/interactions.js +81 -0
- package/dist/harness/interactive-tools.js +97 -0
- package/dist/harness/loader.js +243 -1
- package/dist/harness/packages.js +7 -0
- package/dist/harness/published-hooks.js +208 -0
- package/dist/harness/seam-host.js +136 -1
- package/dist/harness/settings.js +41 -0
- package/dist/history/git.js +6 -2
- package/dist/jobs/fire.js +10 -0
- package/dist/jobs/index.js +3 -0
- package/dist/jobs/observe.js +27 -2
- package/dist/jobs/spec.js +15 -1
- package/dist/server/conversation-ops.js +31 -0
- package/dist/server/core-views/jobs-form.js +335 -0
- package/dist/server/core-views/jobs.js +300 -15
- package/dist/server/core-views/skills-tree.js +3 -12
- package/dist/server/core-views/skills.js +11 -23
- package/dist/server/host-bridge.js +53 -1
- package/dist/server/index.js +2 -1
- package/dist/server/routes/extensions.js +5 -0
- package/dist/server/routes/interactions.js +48 -1
- package/dist/server/routes/meta.js +17 -0
- package/dist/server/routes/providers.js +65 -8
- package/dist/server/routes/submissions.js +3 -0
- package/dist/settings/registry.js +18 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +12 -0
- package/node_modules/@sigrun/shared/dist/decisions.d.ts +95 -0
- package/node_modules/@sigrun/shared/dist/decisions.js +16 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +32 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +58 -0
- package/node_modules/@sigrun/shared/package.json +6 -2
- package/package.json +8 -8
- package/webui/dist/assets/index-By63QKOA.js +55 -0
- package/webui/dist/assets/index-D5KowS7k.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-B3uMNF59.css +0 -1
- package/webui/dist/assets/index-BXsCyod4.js +0 -55
package/dist/extension.d.ts
CHANGED
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
* validated candidate shape and a compile-time key assertion, so the
|
|
33
33
|
* implementation and this contract cannot drift silently.
|
|
34
34
|
*/
|
|
35
|
-
import type { AssistantMessage, Provider } from "@earendil-works/pi-ai";
|
|
35
|
+
import type { AssistantMessage, Provider, TSchema } from "@earendil-works/pi-ai";
|
|
36
36
|
import type { ConversationDocToken, ConversationId, Cursor, EntryId, EntryQuery, EntryRecord, Extension, InputSubmissionDraft, JsonObject, ModelRef, Page, SubmissionId } from "@earendil-works/pi-durable";
|
|
37
37
|
import type { Access, Sharing } from "@sigrun/shared/access";
|
|
38
|
+
import type { DecisionAnswers, DecisionLimits, DecisionRequest, DecisionResult } from "@sigrun/shared/decisions";
|
|
38
39
|
import type { AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
|
|
39
40
|
import type { Action, CoreTarget, DataOps, IconName, Tree } from "@sigrun/shared/blocks";
|
|
40
41
|
import type { UserNotice, UserNoticeInput } from "@sigrun/shared/notices";
|
|
@@ -132,6 +133,10 @@ export type ExtensionLifecyclePayload = {
|
|
|
132
133
|
readonly status: "answered" | "skipped" | "expired" | "cancelled";
|
|
133
134
|
/** The answering user (C5 attribution), when a human answered. */
|
|
134
135
|
readonly answeredByUser?: string;
|
|
136
|
+
/** How a DECLINE landed (ask-user Task 1/2): "button" via the decline
|
|
137
|
+
* route/host method, "message" via an admitted decline message. Set
|
|
138
|
+
* only on a declined ("skipped") record. */
|
|
139
|
+
readonly declinedVia?: "button" | "message";
|
|
135
140
|
/** The STORED (masked-by-construction) answer values — render-safe. */
|
|
136
141
|
readonly answer?: Readonly<Record<string, unknown>>;
|
|
137
142
|
readonly owner?: string;
|
|
@@ -274,8 +279,28 @@ export interface CurationTarget {
|
|
|
274
279
|
readonly name: string;
|
|
275
280
|
/** Tools the review may call to write this target; registered by this extension. */
|
|
276
281
|
readonly tools: readonly string[];
|
|
282
|
+
/**
|
|
283
|
+
* Read-only tools the review may call (knowledge C5): model-visible
|
|
284
|
+
* for serving and allowed inside a review, but NEVER wrapped into a
|
|
285
|
+
* review hint and never gated — the loader's wrap feed grows from
|
|
286
|
+
* `tools` only. Registered by this extension; disjoint from `tools`.
|
|
287
|
+
*/
|
|
288
|
+
readonly readTools?: readonly string[];
|
|
277
289
|
/** Routing guidance for one user; `[]` = unavailable for this user. */
|
|
278
290
|
guidance(user: string): readonly string[] | Promise<readonly string[]>;
|
|
291
|
+
/**
|
|
292
|
+
* Whether this target wants the conversation reviewed (the fork path
|
|
293
|
+
* only — mixed-authorship conversations go to the idle digest). Core
|
|
294
|
+
* bounds each call at 2 s; a throw or timeout counts as `false`.
|
|
295
|
+
*/
|
|
296
|
+
wantsReview?(user: string, conversationId: ConversationId): boolean | Promise<boolean>;
|
|
297
|
+
/**
|
|
298
|
+
* Called after a fork review of the conversation COMPLETED (after
|
|
299
|
+
* the result is recorded) — the target's signal that the review
|
|
300
|
+
* consumed its hints. Not called for any other status. Errors are
|
|
301
|
+
* caught and logged; a target must not rely on being called.
|
|
302
|
+
*/
|
|
303
|
+
reviewed?(user: string, conversationId: ConversationId): void | Promise<void>;
|
|
279
304
|
/** Revert one change this target reported. Required. */
|
|
280
305
|
undo(user: string, changeId: string, ctx: HandlerContext): Promise<void>;
|
|
281
306
|
/** Optional idle maintenance, called by the curator's idle pass. */
|
|
@@ -409,6 +434,8 @@ export type CurationEligibility = {
|
|
|
409
434
|
export type CurationTargetInfo = {
|
|
410
435
|
readonly id: string;
|
|
411
436
|
readonly tools: readonly string[];
|
|
437
|
+
/** The target's declared read tools (C5; absent when it has none). */
|
|
438
|
+
readonly readTools?: readonly string[];
|
|
412
439
|
readonly guidance: readonly string[];
|
|
413
440
|
readonly maintain: boolean;
|
|
414
441
|
};
|
|
@@ -453,8 +480,15 @@ export type SigrunCurationHost = {
|
|
|
453
480
|
modelCallsSince(id: ConversationId, entryId?: EntryId): Promise<number>;
|
|
454
481
|
/** Read and clear the conversation's pending review hints (A8's doc). */
|
|
455
482
|
takeHints(id: ConversationId): Promise<readonly CurationHint[]>;
|
|
456
|
-
/** Targets whose tools are all in the conversation's selection (no id: all). */
|
|
483
|
+
/** Targets whose tools (and readTools) are all in the conversation's selection (no id: all). */
|
|
457
484
|
targets(user: string, id?: ConversationId): Promise<readonly CurationTargetInfo[]>;
|
|
485
|
+
/**
|
|
486
|
+
* Whether any target served for `(user, id)` requests a review (C5):
|
|
487
|
+
* `true` when at least one served target's `wantsReview` answers
|
|
488
|
+
* true. Each target's call is bounded (2 s); a throw or timeout
|
|
489
|
+
* counts as `false`. Fork-path only — the idle digest never asks.
|
|
490
|
+
*/
|
|
491
|
+
wantsReview(user: string, id: ConversationId): Promise<boolean>;
|
|
458
492
|
/** The conversation's model-visible tool names (for the allow-list). */
|
|
459
493
|
tools(id: ConversationId): Promise<readonly string[]>;
|
|
460
494
|
/** Fork the conversation at its newest entry and drive the review run. */
|
|
@@ -520,6 +554,40 @@ export interface ProviderReadiness {
|
|
|
520
554
|
* three-state source `/v1/auth/check` reports verbatim.
|
|
521
555
|
*/
|
|
522
556
|
export type ReadinessTracker = () => ProviderReadiness;
|
|
557
|
+
/** One decision model: a bare id plus optional limits narrower than the built-in caps. */
|
|
558
|
+
export interface DecisionModelInfo {
|
|
559
|
+
readonly id: string;
|
|
560
|
+
readonly limits?: DecisionLimits;
|
|
561
|
+
}
|
|
562
|
+
/** The context core stamps on every decision-provider call. */
|
|
563
|
+
export interface DecisionContext {
|
|
564
|
+
/** The bare model id (the `"<provider>/<model>"` spec's second half). */
|
|
565
|
+
readonly model: string;
|
|
566
|
+
/** auth.json[id].apiKey, else the ambient `credentialEnv`; never `"default"`. */
|
|
567
|
+
readonly credential: string | undefined;
|
|
568
|
+
readonly signal: AbortSignal;
|
|
569
|
+
/** The calling extension's slug, core-stamped (`"sigrun"` for the core tool). */
|
|
570
|
+
readonly caller: string;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* One decision-provider contribution: typed questions (noul / choice /
|
|
574
|
+
* score) answered over a JSON state. A factory result carrying only
|
|
575
|
+
* `decisionProviders` is a valid contributor-only result.
|
|
576
|
+
*/
|
|
577
|
+
export interface DecisionProviderContribution {
|
|
578
|
+
/** `^[a-z][a-z0-9-]*$`; the auth.json key and the model prefix. */
|
|
579
|
+
readonly id: string;
|
|
580
|
+
/** Display name. */
|
|
581
|
+
readonly label?: string;
|
|
582
|
+
/** Ambient env fallback core reads when auth.json has no key. */
|
|
583
|
+
readonly credentialEnv?: string;
|
|
584
|
+
/** The fixed model list (restart-required). */
|
|
585
|
+
readonly models: readonly DecisionModelInfo[];
|
|
586
|
+
/** The chat providers' three-state readiness tracker. */
|
|
587
|
+
readonly readiness?: ReadinessTracker;
|
|
588
|
+
/** May throw; core maps a throw to `failed`. */
|
|
589
|
+
evaluate(request: DecisionRequest, ctx: DecisionContext): Promise<DecisionAnswers>;
|
|
590
|
+
}
|
|
523
591
|
/** One provider contribution: a pi-ai `createProvider()` result plus optional boot hints. */
|
|
524
592
|
export interface ProviderContribution {
|
|
525
593
|
readonly provider: Provider;
|
|
@@ -1017,6 +1085,12 @@ export interface SurfaceDef<P extends "view" | "panel" | "settings"> {
|
|
|
1017
1085
|
readonly placement: P;
|
|
1018
1086
|
/** Data is identical for every principal with access (§7.3). */
|
|
1019
1087
|
readonly sharedData?: boolean;
|
|
1088
|
+
/** Panels only: the channels attach this panel with
|
|
1089
|
+
* `params: { conversationId }` of the OPEN conversation, so one
|
|
1090
|
+
* instance follows the conversation the user is in. The id is
|
|
1091
|
+
* client-supplied — the extension must check the caller's access to
|
|
1092
|
+
* that conversation before rendering or acting on it. */
|
|
1093
|
+
readonly perConversation?: boolean;
|
|
1020
1094
|
/** Channels refetch data on this interval while visible (min 5000). */
|
|
1021
1095
|
readonly refreshMs?: number;
|
|
1022
1096
|
/** §7.3; default: canonical JSON of params. The principal argument
|
|
@@ -1163,6 +1237,15 @@ export interface SigrunHost {
|
|
|
1163
1237
|
}): Promise<boolean>;
|
|
1164
1238
|
/** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
|
|
1165
1239
|
settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
|
|
1240
|
+
/**
|
|
1241
|
+
* Decline a pending DECLINABLE interaction as a human (ask-user Task
|
|
1242
|
+
* 1): status "skipped", `declinedVia: "button"`. `opts.user` names
|
|
1243
|
+
* the declining human (the same attribution `answerInteraction`
|
|
1244
|
+
* stamps); false when the record is not pending or not declinable.
|
|
1245
|
+
*/
|
|
1246
|
+
declineInteraction(conversationId: ConversationId, callId: string, context: HandlerContext, opts?: {
|
|
1247
|
+
readonly user?: string;
|
|
1248
|
+
}): Promise<boolean>;
|
|
1166
1249
|
/**
|
|
1167
1250
|
* Inspect one interaction record WITHOUT consuming anything: returns
|
|
1168
1251
|
* the same stored projection as {@link SigrunHost.snapshotInteraction}
|
|
@@ -1318,6 +1401,33 @@ export interface SigrunHost {
|
|
|
1318
1401
|
/** Read-only job lookup; retired and unknown ids resolve to `undefined`. */
|
|
1319
1402
|
readonly jobs: {
|
|
1320
1403
|
get(id: string): Promise<HostJobInfo | undefined>;
|
|
1404
|
+
/**
|
|
1405
|
+
* The job whose fire is CURRENTLY running in this conversation:
|
|
1406
|
+
* the job with `lastRun.outcome === "running"` and
|
|
1407
|
+
* `lastRun.conversationId === conversationId` — `undefined` when no
|
|
1408
|
+
* job fire is running there. `owner` is the EFFECTIVE owner (the
|
|
1409
|
+
* same value `get` returns: an ownerless or orphaned job resolves
|
|
1410
|
+
* to the primary admin). Two matching runs resolve to the newest
|
|
1411
|
+
* `startedAt`.
|
|
1412
|
+
*/
|
|
1413
|
+
activeRun(conversationId: ConversationId): Promise<{
|
|
1414
|
+
readonly jobId: string;
|
|
1415
|
+
readonly owner: string | undefined;
|
|
1416
|
+
} | undefined>;
|
|
1417
|
+
/**
|
|
1418
|
+
* Whether the conversation's CURRENT run is job-driven (ask-user
|
|
1419
|
+
* Task 3, spec §6): true when the session meta carries a `jobId`
|
|
1420
|
+
* (a `conversation: "new"` run), or any input of the conversation's
|
|
1421
|
+
* current run (`LiveDoc` `run.inputs`, present exactly while busy)
|
|
1422
|
+
* resolves to a submission whose requestId carries the job prefix
|
|
1423
|
+
* (`job:<fireKey>`, the prefix `jobs/fire.ts` stamps on both fire
|
|
1424
|
+
* shapes). Interactive tools call this in execute and refuse with an
|
|
1425
|
+
* error telling the model to escalate instead — the execute-time
|
|
1426
|
+
* guard for the conversations the creation-time filter cannot reach
|
|
1427
|
+
* (a job firing into an existing conversation; defence in depth
|
|
1428
|
+
* should the filter ever miss).
|
|
1429
|
+
*/
|
|
1430
|
+
isJobRun(conversationId: ConversationId, context: HandlerContext): Promise<boolean>;
|
|
1321
1431
|
};
|
|
1322
1432
|
/**
|
|
1323
1433
|
* Send one explicit notice through every registered notifier. Validation
|
|
@@ -1391,10 +1501,54 @@ export interface SigrunHost {
|
|
|
1391
1501
|
};
|
|
1392
1502
|
/**
|
|
1393
1503
|
* The served model catalog (gateway C4b): the SAME list `GET
|
|
1394
|
-
* /v1/models` serves (picker entries — no provider internals)
|
|
1504
|
+
* /v1/models` serves (picker entries — no provider internals), plus
|
|
1505
|
+
* one-shot completions with NO conversation (warden Task 4 —
|
|
1506
|
+
* skill-card generation is the first consumer).
|
|
1395
1507
|
*/
|
|
1396
1508
|
readonly models: {
|
|
1397
1509
|
list(): Promise<readonly ModelEntry[]>;
|
|
1510
|
+
/**
|
|
1511
|
+
* Run ONE one-shot completion and return its concatenated text
|
|
1512
|
+
* parts (non-text parts skipped) plus the `provider/id` of the
|
|
1513
|
+
* model actually used. Model resolution: `input.model` wins;
|
|
1514
|
+
* else the live settings' `namingModel` when it is a string
|
|
1515
|
+
* (`false` counts as unset — the utility role's binding); else
|
|
1516
|
+
* the boot default model. THROWS (never resolves silently) on an
|
|
1517
|
+
* unserved model (naming it), a timeout (default 60 000 ms), or
|
|
1518
|
+
* a `stopReason` of `error`/`aborted`.
|
|
1519
|
+
*/
|
|
1520
|
+
complete(input: {
|
|
1521
|
+
readonly model?: string;
|
|
1522
|
+
readonly role?: "utility";
|
|
1523
|
+
readonly systemPrompt?: string;
|
|
1524
|
+
readonly prompt: string;
|
|
1525
|
+
readonly timeoutMs?: number;
|
|
1526
|
+
readonly signal?: AbortSignal;
|
|
1527
|
+
}): Promise<{
|
|
1528
|
+
readonly text: string;
|
|
1529
|
+
readonly model: string;
|
|
1530
|
+
}>;
|
|
1531
|
+
};
|
|
1532
|
+
/**
|
|
1533
|
+
* Decision providers (warden Task 5 — the decision-providers spec):
|
|
1534
|
+
* typed questions (noul / choice / score) answered over a JSON state.
|
|
1535
|
+
* `evaluate` NEVER throws; failures arrive as the `ok: false` half of
|
|
1536
|
+
* {@link DecisionResult}. Model resolution: `opts.model` wins; else
|
|
1537
|
+
* the live `decisionModel` setting; else the first model of the
|
|
1538
|
+
* first-registered decision provider.
|
|
1539
|
+
*/
|
|
1540
|
+
readonly decisions: {
|
|
1541
|
+
evaluate(request: DecisionRequest, opts?: {
|
|
1542
|
+
model?: string;
|
|
1543
|
+
signal?: AbortSignal;
|
|
1544
|
+
timeoutMs?: number;
|
|
1545
|
+
}): Promise<DecisionResult>;
|
|
1546
|
+
/** Every decision model, with its provider's readiness status. */
|
|
1547
|
+
list(): Promise<readonly {
|
|
1548
|
+
model: string;
|
|
1549
|
+
provider: string;
|
|
1550
|
+
status: ProviderReadiness["status"];
|
|
1551
|
+
}[]>;
|
|
1398
1552
|
};
|
|
1399
1553
|
/**
|
|
1400
1554
|
* The slash-command surface (gateway C4b): the merged catalog for
|
|
@@ -1426,6 +1580,28 @@ export interface SigrunHost {
|
|
|
1426
1580
|
* for the bridge, never a silent drop.
|
|
1427
1581
|
*/
|
|
1428
1582
|
readonly curation: SigrunCurationHost;
|
|
1583
|
+
/**
|
|
1584
|
+
* The published-hook surface (knowledge C7): look up a hook another
|
|
1585
|
+
* extension published (`get` returns `undefined` while loading is
|
|
1586
|
+
* still running and for unowned names) or call it through the core
|
|
1587
|
+
* registry. `call` NEVER throws: an unowned name is `absent`, a
|
|
1588
|
+
* payload the owner's schema rejects is `rejected`, and a provider
|
|
1589
|
+
* failure is `failed` with a reason. A call issued before loading
|
|
1590
|
+
* finishes waits for it — never `await` one inside a factory body
|
|
1591
|
+
* (stash the host; call from a tool, handler, worker, or job).
|
|
1592
|
+
*/
|
|
1593
|
+
readonly publishedHooks: {
|
|
1594
|
+
get(name: string): {
|
|
1595
|
+
provider: string;
|
|
1596
|
+
input: TSchema;
|
|
1597
|
+
output?: TSchema;
|
|
1598
|
+
} | undefined;
|
|
1599
|
+
call(name: string, payload: unknown, opts?: {
|
|
1600
|
+
user?: string;
|
|
1601
|
+
conversationId?: ConversationId;
|
|
1602
|
+
abortSignal?: AbortSignal;
|
|
1603
|
+
}): Promise<PublishedHookResult>;
|
|
1604
|
+
};
|
|
1429
1605
|
}
|
|
1430
1606
|
/**
|
|
1431
1607
|
* The documented factory-result contract: EVERY field an extension may
|
|
@@ -1449,6 +1625,8 @@ export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
|
1449
1625
|
readonly name?: string;
|
|
1450
1626
|
/** Provider contributions, merged into the boot model catalog (last-registered-wins, collisions recorded). */
|
|
1451
1627
|
readonly providers?: readonly ProviderContribution[];
|
|
1628
|
+
/** Decision-provider contributions (warden Task 5); last-registered wins per id. */
|
|
1629
|
+
readonly decisionProviders?: readonly DecisionProviderContribution[];
|
|
1452
1630
|
/** External identity providers; core qualifies ids as `login:<slug>:<id>`. */
|
|
1453
1631
|
readonly loginProviders?: readonly LoginProviderContribution[];
|
|
1454
1632
|
/** Webui-bundle contributions; each `dir` anchors at the extension ENTRY's directory. */
|
|
@@ -1459,6 +1637,17 @@ export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
|
1459
1637
|
readonly runSettled?: ExtensionRunSettledHandler;
|
|
1460
1638
|
/** Declared interaction kinds (the seam's write gate + capability inventory). */
|
|
1461
1639
|
readonly interactionKinds?: readonly string[];
|
|
1640
|
+
/**
|
|
1641
|
+
* Tool names this extension registers that need a human present
|
|
1642
|
+
* (ask-user Task 3, spec §6). Validated at load like
|
|
1643
|
+
* `interactionKinds` (each name must name a tool this extension's
|
|
1644
|
+
* `extension.tools` registers; a bad declaration quarantines). Core
|
|
1645
|
+
* hides the named tools in every non-interactive run (job run
|
|
1646
|
+
* conversations, curator runs); in a job-fired EXISTING conversation
|
|
1647
|
+
* the tool stays listed and must refuse at execute time — guard with
|
|
1648
|
+
* `host.jobs.isJobRun`.
|
|
1649
|
+
*/
|
|
1650
|
+
readonly interactiveTools?: readonly string[];
|
|
1462
1651
|
/** Declared ambient surface keys (the seam's write gate + capability inventory). */
|
|
1463
1652
|
readonly ambientKinds?: readonly string[];
|
|
1464
1653
|
/** Idle workers (the curator contract — `wakeOn` only, `every` retired). */
|
|
@@ -1506,6 +1695,82 @@ export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
|
1506
1695
|
readonly gateway?: GatewayEngine;
|
|
1507
1696
|
/** Declared surfaces — views and panels (UI-blocks U3, spec §7.1; the service lands in Task 2). */
|
|
1508
1697
|
readonly surfaces?: SurfacesContribution;
|
|
1698
|
+
/**
|
|
1699
|
+
* Published hooks (knowledge C7): named, schema-validated calls this
|
|
1700
|
+
* extension offers OTHER extensions through `host.publishedHooks`.
|
|
1701
|
+
* Core compiles each schema once at load; a malformed declaration
|
|
1702
|
+
* (bad name, duplicate within the extension, non-object input,
|
|
1703
|
+
* non-function `run`, a schema that fails to compile) quarantines
|
|
1704
|
+
* the extension. The first configured provider of a name owns it;
|
|
1705
|
+
* configured providers beat built-ins.
|
|
1706
|
+
*/
|
|
1707
|
+
readonly publishedHooks?: readonly PublishedHook[];
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* A published hook (knowledge plan, Task 1): a named, schema-validated
|
|
1711
|
+
* call one extension publishes for OTHER extensions to invoke through
|
|
1712
|
+
* the core registry (the extension-to-extension seam). `name` must
|
|
1713
|
+
* match `/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/` (at least one dot,
|
|
1714
|
+
* at most 128 chars). `run` receives the validated payload plus the
|
|
1715
|
+
* {@link PublishedHookCall} context and resolves with the result.
|
|
1716
|
+
*/
|
|
1717
|
+
export interface PublishedHook {
|
|
1718
|
+
readonly name: string;
|
|
1719
|
+
readonly description?: string;
|
|
1720
|
+
readonly input: TSchema;
|
|
1721
|
+
readonly output?: TSchema;
|
|
1722
|
+
run(payload: unknown, call: PublishedHookCall): Promise<unknown>;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* The call context a published hook's `run` sees: the calling extension's
|
|
1726
|
+
* name plus the resolved identity. `user` comes from the conversation's
|
|
1727
|
+
* acting user when a `conversationId` is given, else the caller's
|
|
1728
|
+
* declared user; a caller-declared user that disagrees with the
|
|
1729
|
+
* conversation owner is rejected before the provider ever runs.
|
|
1730
|
+
*/
|
|
1731
|
+
export interface PublishedHookCall {
|
|
1732
|
+
readonly caller: string;
|
|
1733
|
+
readonly user: string | undefined;
|
|
1734
|
+
readonly conversationId: ConversationId | undefined;
|
|
1735
|
+
readonly abortSignal: AbortSignal | undefined;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* The outcome of one registry call. `absent` — no provider owns the
|
|
1739
|
+
* name. `ok` — the provider ran and produced a valid result. `rejected`
|
|
1740
|
+
* — the call never ran (oversize/unstringifiable payload, schema
|
|
1741
|
+
* mismatch, or identity disagreement). `failed` — the provider ran (or
|
|
1742
|
+
* was interrupted) and could not deliver: `threw`, `timeout`, `aborted`,
|
|
1743
|
+
* `bad-output`, `too-large`, or `too-deep` (depth bound).
|
|
1744
|
+
*/
|
|
1745
|
+
export type PublishedHookResult = {
|
|
1746
|
+
status: "absent";
|
|
1747
|
+
} | {
|
|
1748
|
+
status: "ok";
|
|
1749
|
+
provider: string;
|
|
1750
|
+
value: unknown;
|
|
1751
|
+
} | {
|
|
1752
|
+
status: "rejected";
|
|
1753
|
+
provider: string;
|
|
1754
|
+
errors: {
|
|
1755
|
+
path: string;
|
|
1756
|
+
message: string;
|
|
1757
|
+
}[];
|
|
1758
|
+
} | {
|
|
1759
|
+
status: "failed";
|
|
1760
|
+
provider: string;
|
|
1761
|
+
reason: "threw" | "timeout" | "aborted" | "bad-output" | "too-large" | "too-deep";
|
|
1762
|
+
error: string;
|
|
1763
|
+
};
|
|
1764
|
+
/**
|
|
1765
|
+
* One extension's declaration of a published hook, in loader order.
|
|
1766
|
+
* `builtin` marks core-bundled extensions: when a configured extension
|
|
1767
|
+
* shadows a built-in the warning names replacing the built-in instead
|
|
1768
|
+
* of reordering the extension list.
|
|
1769
|
+
*/
|
|
1770
|
+
export interface PublishedHookDeclaration {
|
|
1771
|
+
readonly extension: string;
|
|
1772
|
+
readonly builtin: boolean;
|
|
1773
|
+
readonly hook: PublishedHook;
|
|
1509
1774
|
}
|
|
1510
1775
|
/**
|
|
1511
1776
|
* The extension factory: sigrun calls it at load, passing the host
|
package/dist/harness/boot.js
CHANGED
|
@@ -11,7 +11,7 @@ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { Harness, } from "@earendil-works/pi-durable";
|
|
13
13
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
14
|
-
import { effectiveMethods } from "../config.js";
|
|
14
|
+
import { DECISION_TIMEOUT_ENV, effectiveMethods } from "../config.js";
|
|
15
15
|
import { requiresProxyVouch } from "../server/auth/proxy-only.js";
|
|
16
16
|
import { wireActivity } from "./activity.js";
|
|
17
17
|
import { snapshotBeforeMigration } from "./migration-snapshot.js";
|
|
@@ -29,6 +29,9 @@ import { AuthStore } from "./auth.js";
|
|
|
29
29
|
import { openIdentityStores, ensureSetupCode, } from "./identity/index.js";
|
|
30
30
|
import { MEMORY_SECTION_KEY, noopMemoryExtension } from "./memory-default.js";
|
|
31
31
|
import { createSeamHost, } from "./seam-host.js";
|
|
32
|
+
import { buildPublishedHookRegistry, } from "./published-hooks.js";
|
|
33
|
+
import { buildDecisionRegistry, } from "./decisions.js";
|
|
34
|
+
import { buildDecisionEvaluateTool, createDecisionsExtension, } from "./decision-tool.js";
|
|
32
35
|
import { contextFilesExtension } from "./sections/context-files.js";
|
|
33
36
|
import { createJobStateExtension } from "./sections/job-state.js";
|
|
34
37
|
import { modelGuidanceExtension } from "./sections/model-guidance.js";
|
|
@@ -653,6 +656,15 @@ export async function openHarness(config, options = {}) {
|
|
|
653
656
|
// can show and update interactive block surfaces in chat — the same
|
|
654
657
|
// core-owned slot as the coding/search tools, never quarantined.
|
|
655
658
|
registry.install(blocksToolsExtension);
|
|
659
|
+
// The decision tool (warden Task 7): installed HERE with an empty
|
|
660
|
+
// tool list — the placeholder holding the core `sigrun.decisions`
|
|
661
|
+
// name — because the decision declarations are only known AFTER the
|
|
662
|
+
// loader runs. Once the registry is built (below), boot RE-INSTALLS
|
|
663
|
+
// the extension with `decision_evaluate` when the registry lists ≥ 1
|
|
664
|
+
// decision model (`registry.install` replaces a same-name extension
|
|
665
|
+
// in place); a no-provider boot keeps the placeholder and the tool
|
|
666
|
+
// is never offered.
|
|
667
|
+
registry.install(createDecisionsExtension());
|
|
656
668
|
// Extension packages load before `Harness.open` reads the registry
|
|
657
669
|
// (spec §3 boot order). Failures quarantine and boot continues. §3
|
|
658
670
|
// factory-result producers are retained alongside the outcomes (Task 9e).
|
|
@@ -732,6 +744,31 @@ export async function openHarness(config, options = {}) {
|
|
|
732
744
|
});
|
|
733
745
|
const bindHarness = (harness) => release(harness);
|
|
734
746
|
const harnessOf = () => harnessPromise;
|
|
747
|
+
// The published-hook registry slot (knowledge C7): the deferred MUST
|
|
748
|
+
// exist before `loadExtensions` runs — seam hosts are built inside the
|
|
749
|
+
// loader's hostContextFactory and close over this slot. Boot builds
|
|
750
|
+
// the registry from the loader's declarations right after
|
|
751
|
+
// `loadExtensions` returns and resolves the deferred; a
|
|
752
|
+
// `host.publishedHooks.call` issued during loading waits on it
|
|
753
|
+
// (the harnessPromise pattern), a `get` before then reads
|
|
754
|
+
// `undefined`.
|
|
755
|
+
let publishRegistryReady;
|
|
756
|
+
const publishedHooksReady = new Promise((resolve) => {
|
|
757
|
+
publishRegistryReady = resolve;
|
|
758
|
+
});
|
|
759
|
+
const publishedHooksSlot = { registry: undefined };
|
|
760
|
+
// The decision-registry slot (warden Task 6): the SAME deferred
|
|
761
|
+
// discipline as the published-hook slot above — seam hosts are built
|
|
762
|
+
// inside the loader's hostContextFactory (before the registry can
|
|
763
|
+
// exist) and close over this slot. Boot builds the registry from the
|
|
764
|
+
// loader's declarations right after `loadExtensions` returns and
|
|
765
|
+
// resolves the deferred; a `host.decisions.evaluate` issued from a
|
|
766
|
+
// factory body waits on it.
|
|
767
|
+
let decisionRegistryReady;
|
|
768
|
+
const decisionsReady = new Promise((resolve) => {
|
|
769
|
+
decisionRegistryReady = resolve;
|
|
770
|
+
});
|
|
771
|
+
const decisionsSlot = { registry: undefined };
|
|
735
772
|
// The boot-shared store trio (Task 5): one SecretsStore + one
|
|
736
773
|
// ExtensionSettingsStore over the roots resolved at the top of boot;
|
|
737
774
|
// each seam host scopes it to its extension's slug (cheap, no I/O —
|
|
@@ -741,7 +778,7 @@ export async function openHarness(config, options = {}) {
|
|
|
741
778
|
const sharedStores = sharedExtensionStores(roots, env, identity.users);
|
|
742
779
|
const seamHosts = new Map();
|
|
743
780
|
const seamKinds = new Map();
|
|
744
|
-
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, settingsChangedBindings, lifecycleBindings, workerBindings, curationBindings, noticeActionBindings, userServices: userServiceBindings, providerContributors, loginProviders, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, routeBindings, commandBindings, notifierBindings, channelBindings, gatewayEngine, surfaces: surfaceBindings, settingsSections: settingsSectionBindings, perUserSlugs: loadedPerUserSlugs, } = await loadExtensions(loaderInputs, registry, {
|
|
781
|
+
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, settingsChangedBindings, lifecycleBindings, workerBindings, curationBindings, noticeActionBindings, publishedHookDeclarations, decisionProviderDeclarations, userServices: userServiceBindings, providerContributors, loginProviders, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, interactiveTools: loadedInteractiveTools, routeBindings, commandBindings, notifierBindings, channelBindings, gatewayEngine, surfaces: surfaceBindings, settingsSections: settingsSectionBindings, perUserSlugs: loadedPerUserSlugs, } = await loadExtensions(loaderInputs, registry, {
|
|
745
782
|
packagesDir,
|
|
746
783
|
// Curator A8 (P9): the wrap feed — the loader grows it with each
|
|
747
784
|
// validated target tool and wraps those tools at their install.
|
|
@@ -764,7 +801,10 @@ export async function openHarness(config, options = {}) {
|
|
|
764
801
|
}, sharedStores, {
|
|
765
802
|
declared: (slug) => perUserSlugs.includes(slug),
|
|
766
803
|
set: (slug, user, tools) => userTools.set(slug, user, tools),
|
|
767
|
-
}, userServices
|
|
804
|
+
}, userServices, {
|
|
805
|
+
publishedHooks: { current: publishedHooksSlot, ready: publishedHooksReady },
|
|
806
|
+
decisions: { current: decisionsSlot, ready: decisionsReady },
|
|
807
|
+
});
|
|
768
808
|
host.rekey({ name: extensionName }, kinds);
|
|
769
809
|
// File under the final name only, and only once the load
|
|
770
810
|
// resolved: the PRE-import call must NOT file (its identity is
|
|
@@ -797,6 +837,65 @@ export async function openHarness(config, options = {}) {
|
|
|
797
837
|
// wrapping feed (isCurationTool) and the A9 host.curation surface
|
|
798
838
|
// read it. An empty fold is the no-declarer deploy.
|
|
799
839
|
const curation = buildCurationRegistry(curationBindings);
|
|
840
|
+
// The published-hook registry (knowledge C7): built from the loader's
|
|
841
|
+
// collected declarations the moment `loadExtensions` returns, then the
|
|
842
|
+
// deferred resolves — a `host.publishedHooks.call` issued from a
|
|
843
|
+
// factory body (still in flight) proceeds from here. `actingUser`
|
|
844
|
+
// reads through the harness deferred: a call whose conversation
|
|
845
|
+
// identity resolves during the load window waits for `Harness.open`
|
|
846
|
+
// exactly like the other bridged members. Load order between caller
|
|
847
|
+
// and provider never matters — lookup is at call time.
|
|
848
|
+
publishedHooksSlot.registry = buildPublishedHookRegistry(publishedHookDeclarations, {
|
|
849
|
+
actingUser: async (id) => {
|
|
850
|
+
const h = await harnessPromise;
|
|
851
|
+
return (await h.snapshot(SessionMetaDoc, id, BACKGROUND_CONTEXT))
|
|
852
|
+
?.owner;
|
|
853
|
+
},
|
|
854
|
+
deadlineMs: config.publishedHookDeadlineMs,
|
|
855
|
+
warn: (message) => console.warn(message),
|
|
856
|
+
});
|
|
857
|
+
// The decision registry (warden Task 6): built from the loader's
|
|
858
|
+
// collected declarations the moment `loadExtensions` returns (the
|
|
859
|
+
// published-hook pattern above), then the deferred resolves — a
|
|
860
|
+
// `host.decisions.evaluate` issued from a factory body (still in
|
|
861
|
+
// flight) proceeds from here. The credential resolves PER CALL:
|
|
862
|
+
// `auth.json[id].apiKey` (a live read, so a key written through the
|
|
863
|
+
// providers route applies without a restart) wins over the ambient
|
|
864
|
+
// `credentialEnv` with the auth bridge's `""`-means-unset rule; the
|
|
865
|
+
// magic `"default"` key never applies (it names the default CHAT
|
|
866
|
+
// provider). The settings read is live too: `decisionModel` and
|
|
867
|
+
// `server.decisions.timeoutMs` apply without a restart.
|
|
868
|
+
decisionsSlot.registry = buildDecisionRegistry(decisionProviderDeclarations, {
|
|
869
|
+
credential: (id, credentialEnv) => {
|
|
870
|
+
const stored = authStore.read()[id]?.apiKey;
|
|
871
|
+
if (stored !== undefined)
|
|
872
|
+
return stored;
|
|
873
|
+
return credentialEnv === undefined
|
|
874
|
+
? undefined
|
|
875
|
+
: env[credentialEnv]?.trim() || undefined;
|
|
876
|
+
},
|
|
877
|
+
readSettings: () => settingsStore.read(),
|
|
878
|
+
timeoutCapMs: config.decisions.timeoutMs,
|
|
879
|
+
// The env tier of the timeout knob: env always wins over a live
|
|
880
|
+
// file edit (the config's env-over-file precedence, per call).
|
|
881
|
+
...(env[DECISION_TIMEOUT_ENV] === undefined || env[DECISION_TIMEOUT_ENV] === ""
|
|
882
|
+
? {}
|
|
883
|
+
: { timeoutMs: config.decisions.timeoutMs }),
|
|
884
|
+
now: () => Date.now(),
|
|
885
|
+
warn: (message) => console.warn(message),
|
|
886
|
+
});
|
|
887
|
+
decisionRegistryReady();
|
|
888
|
+
// Warden Task 7: the late fill — swap the placeholder extension for
|
|
889
|
+
// the tool-bearing one when any decision model exists at boot.
|
|
890
|
+
if (decisionsSlot.registry !== undefined && decisionsSlot.registry.list().length > 0) {
|
|
891
|
+
registry.install(createDecisionsExtension([
|
|
892
|
+
buildDecisionEvaluateTool({
|
|
893
|
+
decisions: decisionsSlot.registry,
|
|
894
|
+
settings: settingsStore,
|
|
895
|
+
}),
|
|
896
|
+
]));
|
|
897
|
+
}
|
|
898
|
+
publishRegistryReady();
|
|
800
899
|
for (const binding of userServiceBindings) {
|
|
801
900
|
userServices.register(binding.extension, binding.services, binding.packageDir);
|
|
802
901
|
}
|
|
@@ -925,6 +1024,12 @@ export async function openHarness(config, options = {}) {
|
|
|
925
1024
|
// `typed` entry — there is no user turn to attribute).
|
|
926
1025
|
commandExpansion,
|
|
927
1026
|
envBuilder: undefined,
|
|
1027
|
+
// Ask-user Task 3: job fires hide interactive tools (assigned with
|
|
1028
|
+
// the rest of the late-bound deps after the load completes).
|
|
1029
|
+
// SAFETY: the placeholder is assigned below, before any fireJob
|
|
1030
|
+
// caller can run (the same late-binding pattern as `harness`/
|
|
1031
|
+
// `storage`/`envBuilder` on this deps object).
|
|
1032
|
+
interactiveTools: undefined,
|
|
928
1033
|
// Gateway C7: a deliver-carrying fire warns once per run when no
|
|
929
1034
|
// engine loaded (core ships none — `gatewayEngine` is undefined
|
|
930
1035
|
// unless an extension declared one).
|
|
@@ -1176,6 +1281,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1176
1281
|
// JobsService — happens after this point.
|
|
1177
1282
|
jobRuntimeDeps.harness = harness;
|
|
1178
1283
|
jobRuntimeDeps.storage = storage;
|
|
1284
|
+
jobRuntimeDeps.interactiveTools = new Set(loadedInteractiveTools);
|
|
1179
1285
|
// Drain the restart-limit parks reported during open before scheduling
|
|
1180
1286
|
// resumes. A failed status patch is logged and dropped by applyParkedJobs.
|
|
1181
1287
|
await applyParkedJobs(harness, reports.takeParked(), BACKGROUND_CONTEXT);
|
|
@@ -1252,6 +1358,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1252
1358
|
storage,
|
|
1253
1359
|
curation,
|
|
1254
1360
|
defaultModel,
|
|
1361
|
+
interactiveTools: new Set(loadedInteractiveTools),
|
|
1255
1362
|
stores: curationOf,
|
|
1256
1363
|
skillsOptions: {
|
|
1257
1364
|
userDir: join(roots.configDir, "skills"),
|
|
@@ -1496,6 +1603,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1496
1603
|
reports,
|
|
1497
1604
|
jobRunTotals: () => jobRunTotals,
|
|
1498
1605
|
defaultModel,
|
|
1606
|
+
interactiveTools: new Set(loadedInteractiveTools),
|
|
1499
1607
|
providerCollisions: sink.collisions,
|
|
1500
1608
|
webuiBundles,
|
|
1501
1609
|
webuiBundleCollisions,
|
|
@@ -1511,6 +1619,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1511
1619
|
identity,
|
|
1512
1620
|
memoryHealth,
|
|
1513
1621
|
settings: settingsStore,
|
|
1622
|
+
decisions: decisionsSlot.registry,
|
|
1514
1623
|
extensionStores: sharedStores,
|
|
1515
1624
|
packagesDir,
|
|
1516
1625
|
seamHosts,
|
|
@@ -1535,6 +1644,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1535
1644
|
curation,
|
|
1536
1645
|
curationHost,
|
|
1537
1646
|
curationOf,
|
|
1647
|
+
publishedHooks: () => publishedHooksSlot.registry?.listing() ?? [],
|
|
1538
1648
|
workspaceRootOf: (owner) => workspaceService?.workspaceRoot(owner),
|
|
1539
1649
|
history: { workspace: workspaceHistoryOf },
|
|
1540
1650
|
commandExpansion,
|