@llblab/pi-kit 0.37.0 → 0.38.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 +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +3 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Domain: immutable scope-participation values and command tokens; excludes mode, I/O, lifecycle authority and cancellation.
|
|
2
|
+
import type { StateScope } from "./state.ts";
|
|
3
|
+
|
|
4
|
+
export type ScopeSelection = readonly [StateScope, ...StateScope[]];
|
|
5
|
+
const SCOPE_ORDER = ["global", "cwd", "session"] as const;
|
|
6
|
+
|
|
7
|
+
/** Missing global configuration defaults to all owners; this is not a retained-session fallback. */
|
|
8
|
+
export const DEFAULT_SCOPE_SELECTION: ScopeSelection = Object.freeze([...SCOPE_ORDER]);
|
|
9
|
+
|
|
10
|
+
/** Validate an explicit value and detach it in canonical overlay order, without granting owner acquisition. */
|
|
11
|
+
export function captureScopeSelection(value: unknown): ScopeSelection {
|
|
12
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > SCOPE_ORDER.length) {
|
|
13
|
+
throw new Error("State Flow scopes must be a nonempty unique array of global, cwd, session");
|
|
14
|
+
}
|
|
15
|
+
const requested = new Set<unknown>();
|
|
16
|
+
for (let index = 0; index < value.length; index++) {
|
|
17
|
+
const scope: unknown = value[index];
|
|
18
|
+
if (!Object.hasOwn(value, index) || !SCOPE_ORDER.some((allowed) => allowed === scope) || requested.has(scope)) {
|
|
19
|
+
throw new Error("State Flow scopes must be a nonempty unique array of global, cwd, session");
|
|
20
|
+
}
|
|
21
|
+
requested.add(scope);
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze(SCOPE_ORDER.filter((scope) => requested.has(scope))) as ScopeSelection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Complete only the next unused owner; values replace the full argument prefix. */
|
|
27
|
+
export function scopeSelectionArgumentCompletions(prefix: string): string[] {
|
|
28
|
+
const tokens = prefix.trimStart().split(/\s+/);
|
|
29
|
+
const partial = tokens.pop()!;
|
|
30
|
+
if (new Set(tokens).size !== tokens.length || tokens.some(token => !SCOPE_ORDER.some(scope => scope === token))) return [];
|
|
31
|
+
return SCOPE_ORDER.filter(scope => !tokens.includes(scope) && scope.startsWith(partial))
|
|
32
|
+
.map(scope => [...tokens, scope].join(" "));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Blank command arguments preserve policy; callers must not reinterpret them as all scopes. */
|
|
36
|
+
export function parseScopeSelectionArguments(args: string): ScopeSelection | undefined {
|
|
37
|
+
const tokens = args.trim();
|
|
38
|
+
return tokens.length === 0 ? undefined : captureScopeSelection(tokens.split(/\s+/));
|
|
39
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Domain: semantic-module contracts and isolated context/producer evaluation.
|
|
2
|
+
// Excludes: storage, native admission, scheduling, inference clients and publication.
|
|
3
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { isJsonValue, isObject, type JsonObject, type JsonValue } from "./json.ts";
|
|
5
|
+
|
|
6
|
+
export type ModuleContextContribution =
|
|
7
|
+
| { kind: "data"; value: JsonValue }
|
|
8
|
+
| { kind: "conversation"; indices: readonly number[] };
|
|
9
|
+
|
|
10
|
+
export interface ModuleFrame<Input> {
|
|
11
|
+
mode: "active" | "passive";
|
|
12
|
+
conversation: readonly AgentMessage[];
|
|
13
|
+
input: Input;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Internal trusted package code, not an effects sandbox or a stored-plane registration. */
|
|
17
|
+
export interface SemanticModule<Content extends JsonValue, Input = undefined> {
|
|
18
|
+
readonly immutable: boolean;
|
|
19
|
+
readonly id: string;
|
|
20
|
+
validate(value: unknown): value is Content;
|
|
21
|
+
context(content: Content | undefined, frame: ModuleFrame<Input>): readonly ModuleContextContribution[];
|
|
22
|
+
produce?(content: Content | undefined, input: Input, signal: AbortSignal): Content | undefined | Promise<Content | undefined>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Public host DTOs: shared owners receive neither native conversation nor current prompt. */
|
|
26
|
+
export interface StateFlowModuleSource {
|
|
27
|
+
readonly mode: "active" | "passive";
|
|
28
|
+
readonly scope: "global" | "cwd" | "session";
|
|
29
|
+
readonly conversation: readonly AgentMessage[];
|
|
30
|
+
readonly prompt?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StateFlowModuleFrame<Input extends JsonValue = JsonValue> extends StateFlowModuleSource {
|
|
34
|
+
readonly input: Input | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StateFlowModuleInput<Input extends JsonValue = JsonValue> {
|
|
38
|
+
readonly value: Input;
|
|
39
|
+
/** Verify only the captured source; throw if obsolete. No acquisition or async work. */
|
|
40
|
+
assertCurrent(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Candidate public v1 contract; durable activation requires matching retained metadata. */
|
|
44
|
+
export interface StateFlowModule<Content extends JsonObject = JsonObject, Input extends JsonValue = JsonValue> {
|
|
45
|
+
readonly immutable: boolean;
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly apiVersion: 1;
|
|
48
|
+
readonly schemaVersion: number;
|
|
49
|
+
validate(value: unknown): value is Content;
|
|
50
|
+
captureInput?(frame: StateFlowModuleSource, signal: AbortSignal): StateFlowModuleInput<Input> | Promise<StateFlowModuleInput<Input>>;
|
|
51
|
+
context(content: Content | undefined, frame: StateFlowModuleFrame<Input>): readonly ModuleContextContribution[];
|
|
52
|
+
produce?(content: Content | undefined, frame: StateFlowModuleFrame<Input>, signal: AbortSignal): Content | undefined | Promise<Content | undefined>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Durable identity only: retained even while a definition is unavailable or content deleted. */
|
|
56
|
+
export interface RetainedModuleDescriptor {
|
|
57
|
+
schemaVersion: number;
|
|
58
|
+
immutable: boolean;
|
|
59
|
+
}
|
|
60
|
+
export type RetainedModules = Record<string, RetainedModuleDescriptor>;
|
|
61
|
+
|
|
62
|
+
export function validateRetainedModules(value: unknown): asserts value is RetainedModules {
|
|
63
|
+
if (!isObject(value) || !isJsonValue(value) || !losslessStoredJson(value)) throw new Error("Invalid State Flow retained modules");
|
|
64
|
+
for (const [id, descriptor] of Object.entries(value)) {
|
|
65
|
+
if (!/^x_[a-z][a-z0-9_]*$/.test(id) || !isObject(descriptor)
|
|
66
|
+
|| Object.keys(descriptor).sort().join(",") !== "immutable,schemaVersion"
|
|
67
|
+
|| !Number.isSafeInteger(descriptor.schemaVersion) || (descriptor.schemaVersion as number) <= 0
|
|
68
|
+
|| typeof descriptor.immutable !== "boolean") throw new Error(`Invalid State Flow retained module descriptor: ${id}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Descriptors cannot be removed, recycled or implicitly change schema/policy. */
|
|
73
|
+
export function assertRetainedModules(previous: unknown, next: unknown): void {
|
|
74
|
+
validateRetainedModules(previous);
|
|
75
|
+
validateRetainedModules(next);
|
|
76
|
+
for (const [id, descriptor] of Object.entries(previous)) {
|
|
77
|
+
if (!Object.hasOwn(next, id) || next[id]!.schemaVersion !== descriptor.schemaVersion
|
|
78
|
+
|| next[id]!.immutable !== descriptor.immutable) throw new Error(`State Flow retained module descriptor changed: ${id}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type EvaluatedModuleContext =
|
|
83
|
+
| { kind: "data"; value: JsonValue }
|
|
84
|
+
| { kind: "conversation"; messages: readonly AgentMessage[] };
|
|
85
|
+
|
|
86
|
+
function dense(value: JsonValue): boolean {
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index) || !dense(value[index]!)) return false;
|
|
89
|
+
} else if (isObject(value)) return Object.values(value).every(dense);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function losslessStoredJson(value: JsonValue): boolean {
|
|
94
|
+
if (value === null || typeof value !== "object") return true;
|
|
95
|
+
const keys = Reflect.ownKeys(value);
|
|
96
|
+
if (Array.isArray(value) && keys.length !== value.length + 1) return false;
|
|
97
|
+
return keys.every(key => {
|
|
98
|
+
if (Array.isArray(value) && key === "length") return true;
|
|
99
|
+
const property = Object.getOwnPropertyDescriptor(value, key)!;
|
|
100
|
+
return typeof key === "string" && property.enumerable === true && "value" in property && losslessStoredJson(property.value);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Raw durable planes use JSON object values, independent of any definition callback. */
|
|
105
|
+
export function validateModuleObject(value: unknown): asserts value is JsonObject {
|
|
106
|
+
if (!isObject(value) || !isJsonValue(value) || !dense(value) || !losslessStoredJson(value)) throw new Error("State Flow external content must be a finite dense JSON object");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function capture<Value>(value: Value): Value {
|
|
110
|
+
try { return structuredClone(value); }
|
|
111
|
+
catch { throw new Error("Semantic module inputs must be detachable data"); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function captureJson(value: unknown): JsonValue {
|
|
115
|
+
if (!isJsonValue(value) || !dense(value)) throw new Error("Semantic module content must be finite dense JSON data");
|
|
116
|
+
const copy = capture(value);
|
|
117
|
+
if (!isJsonValue(copy) || !dense(copy)) throw new Error("Semantic module content must be finite dense JSON data");
|
|
118
|
+
return copy;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function contentFor<Content extends JsonValue, Input>(module: SemanticModule<Content, Input>, value: unknown): Content | undefined {
|
|
122
|
+
if (value === undefined) return undefined;
|
|
123
|
+
const copy = captureJson(value), validation = capture(copy);
|
|
124
|
+
if (module.validate(validation) !== true || !isJsonValue(validation) || !dense(validation)) throw new Error(`Invalid content for semantic module ${module.id}`);
|
|
125
|
+
// Validators neither transform the content nor retain the returned candidate.
|
|
126
|
+
return capture(copy) as Content;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Context consumes captured data; conversation selections retain the host's exact original messages. */
|
|
130
|
+
export function evaluateModuleContext<Content extends JsonValue, Input>(
|
|
131
|
+
module: SemanticModule<Content, Input>, content: unknown, frame: ModuleFrame<Input>,
|
|
132
|
+
): readonly EvaluatedModuleContext[] {
|
|
133
|
+
if (frame.mode !== "active" && frame.mode !== "passive") throw new Error("Semantic module context requires Active or Passive mode");
|
|
134
|
+
const original = [...frame.conversation];
|
|
135
|
+
const result = module.context(contentFor(module, content), { mode: frame.mode, conversation: capture(original), input: capture(frame.input) });
|
|
136
|
+
if (!Array.isArray(result)) throw new Error("Semantic module context must return a contribution array");
|
|
137
|
+
let last = -1;
|
|
138
|
+
return Array.from(result, contribution => {
|
|
139
|
+
if (!isObject(contribution)) throw new Error("Invalid semantic module context contribution");
|
|
140
|
+
const keys = Object.keys(contribution).sort().join(",");
|
|
141
|
+
if (contribution.kind === "data" && keys === "kind,value") return { kind: "data" as const, value: captureJson(contribution.value) };
|
|
142
|
+
if (contribution.kind !== "conversation" || keys !== "indices,kind" || !Array.isArray(contribution.indices)) {
|
|
143
|
+
throw new Error("Invalid semantic module context contribution");
|
|
144
|
+
}
|
|
145
|
+
const messages = Array.from(contribution.indices, index => {
|
|
146
|
+
if (typeof index !== "number" || !Number.isSafeInteger(index) || index <= last || index >= original.length) throw new Error("Semantic module conversation indices must be ordered, unique and in range");
|
|
147
|
+
last = index;
|
|
148
|
+
const message = original[index]!;
|
|
149
|
+
if (!message || !["user", "assistant", "toolResult"].includes(message.role)) throw new Error("Semantic modules may select conversation messages, not protocol frames");
|
|
150
|
+
return message;
|
|
151
|
+
});
|
|
152
|
+
return { kind: "conversation" as const, messages };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Isolated public input/context capture; grants neither native nor publication authority. */
|
|
157
|
+
export interface PreparedModuleContext {
|
|
158
|
+
assertCurrent(): void;
|
|
159
|
+
evaluate(accepted?: { content: unknown }): readonly EvaluatedModuleContext[];
|
|
160
|
+
produce(): Promise<JsonObject | undefined>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function cancelableModuleWork<Value>(work: () => Value | Promise<Value>, signal: AbortSignal, checkCurrent: () => void): Promise<Value> {
|
|
164
|
+
const check = () => { signal.throwIfAborted(); checkCurrent(); signal.throwIfAborted(); };
|
|
165
|
+
check();
|
|
166
|
+
let abort!: () => void;
|
|
167
|
+
const canceled = new Promise<never>((_resolve, reject) => { abort = () => reject(signal.reason); signal.addEventListener("abort", abort, { once: true }); });
|
|
168
|
+
try {
|
|
169
|
+
return await Promise.race([Promise.resolve().then(() => { check(); return work(); }).then(value => { check(); return value; }), canceled]);
|
|
170
|
+
} finally { signal.removeEventListener("abort", abort); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function prepareModuleContext<Content extends JsonObject, Input extends JsonValue>(
|
|
174
|
+
module: StateFlowModule<Content, Input>, content: unknown, source: StateFlowModuleSource, signal: AbortSignal, checkCurrent: () => void,
|
|
175
|
+
): Promise<PreparedModuleContext> {
|
|
176
|
+
const { mode, scope, prompt } = source;
|
|
177
|
+
if (mode !== "active" && mode !== "passive" || !["global", "cwd", "session"].includes(scope)) throw new Error("Invalid State Flow module source frame");
|
|
178
|
+
if (scope !== "session" && (source.conversation.length || prompt !== undefined)) throw new Error("Shared module frames cannot acquire Session conversation or prompt");
|
|
179
|
+
const conversation = [...source.conversation];
|
|
180
|
+
const frameSource: StateFlowModuleSource = { mode, scope, conversation, ...(prompt === undefined ? {} : { prompt }) };
|
|
181
|
+
const adapter: SemanticModule<Content, Input | undefined> = { id: module.id, immutable: module.immutable,
|
|
182
|
+
validate: value => module.validate(value),
|
|
183
|
+
context: (value, frame) => module.context(value, { mode, scope, conversation: frame.conversation, input: frame.input, ...(prompt === undefined ? {} : { prompt }) }) };
|
|
184
|
+
const check = () => { signal.throwIfAborted(); checkCurrent(); signal.throwIfAborted(); };
|
|
185
|
+
check();
|
|
186
|
+
const previous = contentFor(adapter, content);
|
|
187
|
+
const receipt = module.captureInput ? await cancelableModuleWork(() => module.captureInput!(capture(frameSource), signal), signal, check) : undefined;
|
|
188
|
+
if (module.captureInput && receipt === undefined || receipt !== undefined && (!isObject(receipt) || typeof receipt.assertCurrent !== "function" || !Object.hasOwn(receipt, "value"))) {
|
|
189
|
+
throw new Error("State Flow module input requires value and synchronous assertCurrent");
|
|
190
|
+
}
|
|
191
|
+
const currentInput = receipt?.assertCurrent.bind(receipt);
|
|
192
|
+
const assertCurrent = () => {
|
|
193
|
+
check();
|
|
194
|
+
const result: unknown = currentInput?.();
|
|
195
|
+
if (result !== undefined) {
|
|
196
|
+
if (result && typeof (result as { then?: unknown }).then === "function") void Promise.resolve(result).catch(() => {});
|
|
197
|
+
throw new Error("State Flow module input assertion must synchronously throw or return void");
|
|
198
|
+
}
|
|
199
|
+
check();
|
|
200
|
+
};
|
|
201
|
+
assertCurrent();
|
|
202
|
+
const input = receipt === undefined ? undefined : captureJson(receipt.value) as Input;
|
|
203
|
+
assertCurrent();
|
|
204
|
+
return Object.freeze({ assertCurrent, produce: async () => {
|
|
205
|
+
assertCurrent();
|
|
206
|
+
if (mode !== "active") throw new Error("State Flow module producers require Active mode");
|
|
207
|
+
if (!module.produce) return undefined;
|
|
208
|
+
const producing: SemanticModule<JsonObject, StateFlowModuleFrame<Input>> = { id: module.id, immutable: module.immutable,
|
|
209
|
+
validate: (value): value is JsonObject => isObject(value) && (Object.keys(value).length === 0 || module.validate(value)),
|
|
210
|
+
context: () => [], produce: (value, frame, activeSignal) => module.produce!(value as Content | undefined, frame, activeSignal) };
|
|
211
|
+
const candidate = await produceModuleContent(producing, previous, { ...frameSource, input }, signal, assertCurrent);
|
|
212
|
+
if (candidate !== undefined) validateModuleObject(candidate);
|
|
213
|
+
assertCurrent();
|
|
214
|
+
return candidate;
|
|
215
|
+
}, evaluate: (accepted?: { content: unknown }) => {
|
|
216
|
+
assertCurrent();
|
|
217
|
+
const result = evaluateModuleContext(adapter, accepted === undefined ? previous : accepted.content, { mode, conversation, input });
|
|
218
|
+
if (scope !== "session" && result.some(item => item.kind === "conversation")) throw new Error("Shared modules cannot replace Session conversation");
|
|
219
|
+
assertCurrent();
|
|
220
|
+
return result;
|
|
221
|
+
} });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Evaluate only: the host must separately bind the captured basis to its existing atomic acceptance. */
|
|
225
|
+
export async function produceModuleContent<Content extends JsonValue, Input>(
|
|
226
|
+
module: SemanticModule<Content, Input>, content: unknown, input: Input, signal: AbortSignal, checkCurrent: () => void,
|
|
227
|
+
): Promise<Content | undefined> {
|
|
228
|
+
const check = () => { signal.throwIfAborted(); checkCurrent(); signal.throwIfAborted(); };
|
|
229
|
+
check();
|
|
230
|
+
const produce = module.produce?.bind(module);
|
|
231
|
+
if (!produce) return undefined;
|
|
232
|
+
const previous = contentFor(module, content), captured = capture(input);
|
|
233
|
+
check();
|
|
234
|
+
return cancelableModuleWork(async () => {
|
|
235
|
+
const value = await produce(previous, captured, signal);
|
|
236
|
+
check();
|
|
237
|
+
return contentFor(module, value);
|
|
238
|
+
}, signal, check);
|
|
239
|
+
}
|