@tomflow/proflow-execution-browser-extension 0.1.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.
Files changed (39) hide show
  1. package/README.md +7 -0
  2. package/conformance.json +1 -0
  3. package/deployment/browser-extension.json +6 -0
  4. package/dist/deployment/adapter.d.ts +61 -0
  5. package/dist/deployment/adapter.js +47 -0
  6. package/dist/deployment/descriptor.d.ts +103 -0
  7. package/dist/deployment/descriptor.js +109 -0
  8. package/dist/extension/background.d.ts +1 -0
  9. package/dist/extension/background.js +752 -0
  10. package/dist/extension/content.d.ts +1 -0
  11. package/dist/extension/content.js +90 -0
  12. package/dist/extension/options.d.ts +1 -0
  13. package/dist/extension/options.js +68 -0
  14. package/dist/extension/side-panel.d.ts +1 -0
  15. package/dist/extension/side-panel.js +262 -0
  16. package/dist/src/bridge.d.ts +26 -0
  17. package/dist/src/bridge.js +288 -0
  18. package/dist/src/collaboration-carrier.d.ts +65 -0
  19. package/dist/src/collaboration-carrier.js +138 -0
  20. package/dist/src/index.d.ts +137 -0
  21. package/dist/src/index.js +779 -0
  22. package/dist/src/runtime-composition.d.ts +97 -0
  23. package/dist/src/runtime-composition.js +124 -0
  24. package/dist/src/system-observer.d.ts +86 -0
  25. package/dist/src/system-observer.js +252 -0
  26. package/dist/src/task-observer.d.ts +118 -0
  27. package/dist/src/task-observer.js +105 -0
  28. package/dist/src/vision.d.ts +73 -0
  29. package/dist/src/vision.js +82 -0
  30. package/extension/background.ts +997 -0
  31. package/extension/content.ts +138 -0
  32. package/extension/options.html +54 -0
  33. package/extension/options.ts +98 -0
  34. package/extension/side-panel.html +77 -0
  35. package/extension/side-panel.ts +349 -0
  36. package/manifest.json +20 -0
  37. package/package.json +58 -0
  38. package/proflow.module.json +127 -0
  39. package/self-install.mjs +27 -0
@@ -0,0 +1,97 @@
1
+ import { type BrowserRealityBridgeOptions } from "./bridge.ts";
2
+ import { type BrowserVisionPort } from "./index.ts";
3
+ type LocalApplicationConfig = {
4
+ endpoint: string;
5
+ token: string;
6
+ };
7
+ export type BrowserExecutorCompositionOptions = {
8
+ platformHost: LocalApplicationConfig;
9
+ bridge: BrowserRealityBridgeOptions;
10
+ vision?: BrowserVisionPort;
11
+ };
12
+ export type BrowserExecutorCompositionFileConfig = {
13
+ platformHost: {
14
+ endpoint: string;
15
+ tokenFile: string;
16
+ };
17
+ bridge: {
18
+ extensionId: string;
19
+ tokenFile: string;
20
+ host?: string;
21
+ port?: number;
22
+ };
23
+ };
24
+ export declare function loadBrowserExecutorCompositionConfig(path: string): Promise<BrowserExecutorCompositionOptions>;
25
+ /**
26
+ * Node-side Browser Executor composition. This deliberately does NOT create an
27
+ * Execution Runtime process. The single formal execution-runtime binary owns
28
+ * Execution truth and must inject `browserExecutor` from this composition.
29
+ */
30
+ export declare function createBrowserExecutorComposition(options: BrowserExecutorCompositionOptions): Promise<Readonly<{
31
+ browserExecutor: Readonly<{
32
+ extensionInstanceId: string;
33
+ parseCarrierIdentity: (raw: string) => {
34
+ roleRef: string;
35
+ workerRef: string | null;
36
+ };
37
+ registerContentSession: (observation: import("./index.ts").BrowserPageObservation) => void;
38
+ isContentSessionCurrent(tabId: number, contentInstanceId: string): boolean;
39
+ classifyProgress(input: {
40
+ pageState: import("./index.ts").BrowserPageState;
41
+ nodeInProgress: boolean;
42
+ millisecondsWithoutProgress: number;
43
+ legitimateWait: boolean;
44
+ }): "EXPECTED_WAIT" | "NORMAL" | "PROGRESS_GAP" | "RUNTIME_STALL";
45
+ inspectScreenshot(tabId: number, observationContext: import("./vision.ts").BrowserVisionObservationContext): Promise<import("./vision.ts").TypedVisionObservation>;
46
+ handlePermissionFallback(tabId: number, continuationRef: string): Promise<{
47
+ status: "WAITING_HUMAN";
48
+ continuationRef: string;
49
+ evidenceRef: string;
50
+ }>;
51
+ getSidePanelSnapshot(): Readonly<{
52
+ extensionInstanceId: string;
53
+ observedAt: string;
54
+ sessions: {
55
+ tabId: number;
56
+ windowId: number;
57
+ url: string;
58
+ contentInstanceId: string;
59
+ pageState: import("./index.ts").BrowserPageState;
60
+ activityKind: import("./index.ts").BrowserActivityKind;
61
+ }[];
62
+ lanes: {
63
+ roleRef: string;
64
+ workerRef: string;
65
+ tabId: number;
66
+ pageState: import("./index.ts").BrowserPageState;
67
+ activityKind: import("./index.ts").BrowserActivityKind;
68
+ currentExecutionRef: string | null;
69
+ continuationRef: string | null;
70
+ lastProgressAt: string;
71
+ }[];
72
+ }>;
73
+ execute: (raw: import("@tomflow/proflow-execution-contracts").ExecutorInvocation) => Promise<import("@tomflow/proflow-execution-contracts").ExecutorResult>;
74
+ observePrecondition: (request: import("@tomflow/proflow-execution-contracts").ExecuteCapabilityRequest) => Promise<import("@tomflow/proflow-execution-contracts").ExecutorPrecondition | undefined>;
75
+ reconcile: (requestRaw: import("@tomflow/proflow-execution-contracts").ExecuteCapabilityRequest, preconditionRaw: import("@tomflow/proflow-execution-contracts").ExecutorPrecondition) => Promise<import("@tomflow/proflow-execution-contracts").ExecutorReconciliation>;
76
+ readArtifact(): Promise<never>;
77
+ recoveryScan(unfinished: Array<{
78
+ request: import("@tomflow/proflow-execution-contracts").ExecuteCapabilityRequest;
79
+ effectStarted: boolean;
80
+ }>): Promise<{
81
+ status: "ALREADY_COMPLETED";
82
+ reconciled: never[];
83
+ } | {
84
+ status: "COMPLETED";
85
+ reconciled: import("@tomflow/proflow-execution-contracts").ExecutorReconciliation[];
86
+ }>;
87
+ }>;
88
+ bridgeEndpoint: string;
89
+ bridgeStatus: () => {
90
+ online: boolean;
91
+ extensionInstanceId: string | null;
92
+ queuedCommands: number;
93
+ pendingCommands: number;
94
+ };
95
+ close: () => Promise<void>;
96
+ }>>;
97
+ export {};
@@ -0,0 +1,124 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { createBrowserRealityBridgeServer, } from "./bridge.js";
4
+ import { createExecutionBrowserExtension, } from "./index.js";
5
+ function record(value, name) {
6
+ if (typeof value !== "object" || value === null || Array.isArray(value))
7
+ throw new TypeError(`${name} must be an object`);
8
+ return value;
9
+ }
10
+ function nonEmpty(value, name) {
11
+ if (typeof value !== "string" || value.length === 0)
12
+ throw new TypeError(`${name} must be a non-empty string`);
13
+ return value;
14
+ }
15
+ async function readSecret(path, name) {
16
+ const info = await stat(path);
17
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0)
18
+ throw new TypeError(`${name} permissions must be owner-only`);
19
+ const value = (await readFile(path, "utf8")).trim();
20
+ if (value.length < 32)
21
+ throw new TypeError(`${name} must contain at least 32 characters`);
22
+ return value;
23
+ }
24
+ export async function loadBrowserExecutorCompositionConfig(path) {
25
+ const absolute = resolve(path);
26
+ const root = dirname(absolute);
27
+ const raw = record(JSON.parse(await readFile(absolute, "utf8")), "browser executor composition config");
28
+ const platformHost = record(raw.platformHost, "platformHost");
29
+ const bridge = record(raw.bridge, "bridge");
30
+ return {
31
+ platformHost: {
32
+ endpoint: nonEmpty(platformHost.endpoint, "platformHost.endpoint"),
33
+ token: await readSecret(resolve(root, nonEmpty(platformHost.tokenFile, "platformHost.tokenFile")), "platformHost token"),
34
+ },
35
+ bridge: {
36
+ extensionId: nonEmpty(bridge.extensionId, "bridge.extensionId"),
37
+ token: await readSecret(resolve(root, nonEmpty(bridge.tokenFile, "bridge.tokenFile")), "bridge token"),
38
+ ...(bridge.host === "127.0.0.1" ? { host: bridge.host } : {}),
39
+ ...(bridge.port !== undefined ? { port: Number(bridge.port) } : {}),
40
+ },
41
+ };
42
+ }
43
+ function applicationConfig(input) {
44
+ const endpoint = new URL(input.endpoint);
45
+ if (endpoint.protocol !== "http:" ||
46
+ !new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
47
+ endpoint.pathname !== "/" ||
48
+ endpoint.search !== "" ||
49
+ endpoint.hash !== "")
50
+ throw new TypeError("platformHost endpoint must be loopback HTTP root");
51
+ if (input.token.length < 32)
52
+ throw new TypeError("platformHost token must contain at least 32 characters");
53
+ return { endpoint: input.endpoint.replace(/\/$/, ""), token: input.token };
54
+ }
55
+ async function invokeApplication(config, operation, input) {
56
+ const response = await fetch(`${config.endpoint}/application/observer`, {
57
+ method: "POST",
58
+ headers: {
59
+ authorization: `Bearer ${config.token}`,
60
+ "content-type": "application/json",
61
+ },
62
+ body: JSON.stringify({ operation, input }),
63
+ });
64
+ const text = await response.text();
65
+ const body = text.length ? JSON.parse(text) : undefined;
66
+ if (!response.ok)
67
+ throw new Error(typeof body === "object" &&
68
+ body !== null &&
69
+ typeof Reflect.get(body, "error") === "string"
70
+ ? String(Reflect.get(body, "error"))
71
+ : "BROWSER_OWNER_TRANSPORT_FAILED");
72
+ return body;
73
+ }
74
+ /**
75
+ * Node-side Browser Executor composition. This deliberately does NOT create an
76
+ * Execution Runtime process. The single formal execution-runtime binary owns
77
+ * Execution truth and must inject `browserExecutor` from this composition.
78
+ */
79
+ export async function createBrowserExecutorComposition(options) {
80
+ const platformHost = applicationConfig(options.platformHost);
81
+ const bridge = await createBrowserRealityBridgeServer(options.bridge);
82
+ try {
83
+ const browserExecutor = createExecutionBrowserExtension({
84
+ browser: bridge.browser,
85
+ ...(options.vision ? { vision: options.vision } : {}),
86
+ task: {
87
+ async getWorkerBinding(taskId, roleRef) {
88
+ return (await invokeApplication(platformHost, "browser.binding", {
89
+ taskId,
90
+ roleRef,
91
+ }));
92
+ },
93
+ async bindWorker(binding) {
94
+ await invokeApplication(platformHost, "browser.bindWorker", binding);
95
+ },
96
+ },
97
+ agent: {
98
+ async getPendingMessage(messageRef) {
99
+ return (await invokeApplication(platformHost, "collaboration.getPending", {
100
+ messageRef,
101
+ }));
102
+ },
103
+ async reportPhysicalDelivery(messageRef, evidenceRef, executionRef) {
104
+ await invokeApplication(platformHost, "collaboration.reportDelivery", {
105
+ messageRef,
106
+ outcome: "DELIVERED",
107
+ evidenceRef,
108
+ executionRef,
109
+ });
110
+ },
111
+ },
112
+ });
113
+ return Object.freeze({
114
+ browserExecutor,
115
+ bridgeEndpoint: bridge.endpoint,
116
+ bridgeStatus: bridge.status,
117
+ close: bridge.close,
118
+ });
119
+ }
120
+ catch (error) {
121
+ await bridge.close();
122
+ throw error;
123
+ }
124
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * System Observer: lowest-priority, read-only system assessment orchestration.
3
+ *
4
+ * Owner domains project bounded snapshots. This caller groups them into concern
5
+ * batches, carries unresolved findings forward explicitly, requests targeted
6
+ * drill-down only when the assessment asks for it, and performs one final
7
+ * global synthesis. It never mutates owner facts and it never owns model
8
+ * scheduling or business state.
9
+ */
10
+ export declare const SYSTEM_OBSERVER_VIEWS: readonly ["task", "worker", "collaboration", "execution", "carrier", "model", "deployment", "artifact"];
11
+ export type SystemObserverView = (typeof SYSTEM_OBSERVER_VIEWS)[number];
12
+ export type SystemObserverPriority = "BACKGROUND" | "LOWEST" | "DEFERRED";
13
+ export type SystemObserverHealth = "HEALTHY" | "DEGRADED" | "CRITICAL" | "UNKNOWN";
14
+ export interface SystemObserverCarryForwardItem {
15
+ hypothesis: string;
16
+ risk?: string;
17
+ evidenceRef?: string;
18
+ confidence: number;
19
+ }
20
+ export interface SystemObserverAssessment {
21
+ scope: string;
22
+ observedAt: string;
23
+ health: SystemObserverHealth;
24
+ findings: string[];
25
+ risks: string[];
26
+ anomalies: string[];
27
+ hypotheses: string[];
28
+ unresolved: string[];
29
+ needsDrilldown: string[];
30
+ evidenceRefs: string[];
31
+ confidence: number;
32
+ carryForward: SystemObserverCarryForwardItem[];
33
+ rationale: string;
34
+ }
35
+ export interface SystemObserverSnapshotPort {
36
+ readView(view: SystemObserverView): Promise<unknown>;
37
+ readDrilldown?(request: {
38
+ topic: string;
39
+ views: readonly SystemObserverView[];
40
+ }): Promise<unknown>;
41
+ }
42
+ export interface SystemObserverReasonRequest {
43
+ assessmentRef: string;
44
+ kind: "CONCERN_BATCH" | "DRILLDOWN" | "GLOBAL_SYNTHESIS";
45
+ scope: string;
46
+ observedAt: string;
47
+ views: Partial<Record<SystemObserverView, unknown>>;
48
+ previousUnresolved: string[];
49
+ previousCarryForward: SystemObserverCarryForwardItem[];
50
+ batchAssessments?: SystemObserverAssessment[];
51
+ drilldown?: Array<{
52
+ topic: string;
53
+ data: unknown;
54
+ }>;
55
+ }
56
+ export interface SystemObserverReasonResult extends Omit<SystemObserverAssessment, "scope" | "observedAt"> {
57
+ scope?: string;
58
+ }
59
+ export type SystemObserverReasonFailure = {
60
+ ok: false;
61
+ errorCode: "CONTEXT_TOO_LARGE" | "REASON_UNAVAILABLE" | "REASON_FAILED";
62
+ };
63
+ export interface SystemObserverResult {
64
+ assessmentRef: string;
65
+ priority: SystemObserverPriority;
66
+ observedAt: string;
67
+ assessments: SystemObserverAssessment[];
68
+ drilldown: Array<{
69
+ topic: string;
70
+ data: unknown;
71
+ }>;
72
+ global: SystemObserverAssessment | null;
73
+ status: "ASSESSED" | "DEFERRED";
74
+ errorCode?: "CONTEXT_TOO_LARGE" | "REASON_UNAVAILABLE" | "REASON_FAILED";
75
+ }
76
+ export declare function createSystemObserver(options: {
77
+ snapshots: SystemObserverSnapshotPort;
78
+ reason?: (input: SystemObserverReasonRequest) => Promise<SystemObserverReasonResult | SystemObserverReasonFailure>;
79
+ idFactory?: () => string;
80
+ now?: () => Date;
81
+ }): Readonly<{
82
+ synthesize: (input?: {
83
+ previousUnresolved?: string[];
84
+ previousCarryForward?: SystemObserverCarryForwardItem[];
85
+ }) => Promise<SystemObserverResult>;
86
+ }>;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * System Observer: lowest-priority, read-only system assessment orchestration.
3
+ *
4
+ * Owner domains project bounded snapshots. This caller groups them into concern
5
+ * batches, carries unresolved findings forward explicitly, requests targeted
6
+ * drill-down only when the assessment asks for it, and performs one final
7
+ * global synthesis. It never mutates owner facts and it never owns model
8
+ * scheduling or business state.
9
+ */
10
+ export const SYSTEM_OBSERVER_VIEWS = [
11
+ "task",
12
+ "worker",
13
+ "collaboration",
14
+ "execution",
15
+ "carrier",
16
+ "model",
17
+ "deployment",
18
+ "artifact",
19
+ ];
20
+ const DEFAULT_CONCERN_BATCHES = [
21
+ { scope: "task-worker", views: ["task", "worker"] },
22
+ { scope: "execution-approval", views: ["execution", "artifact"] },
23
+ { scope: "collaboration-carrier", views: ["collaboration", "carrier"] },
24
+ { scope: "model-deployment-health", views: ["model", "deployment"] },
25
+ ];
26
+ function normalizeAssessment(result, scope, observedAt) {
27
+ return {
28
+ scope: result.scope ?? scope,
29
+ observedAt,
30
+ health: result.health,
31
+ findings: [...result.findings],
32
+ risks: [...result.risks],
33
+ anomalies: [...result.anomalies],
34
+ hypotheses: [...result.hypotheses],
35
+ unresolved: [...result.unresolved],
36
+ needsDrilldown: [...result.needsDrilldown],
37
+ evidenceRefs: [...result.evidenceRefs],
38
+ confidence: result.confidence,
39
+ carryForward: result.carryForward.map((item) => ({ ...item })),
40
+ rationale: result.rationale,
41
+ };
42
+ }
43
+ function isReasonFailure(value) {
44
+ return "ok" in value && value.ok === false;
45
+ }
46
+ function compactView(value) {
47
+ if (typeof value !== "object" || value === null || Array.isArray(value))
48
+ return value;
49
+ const source = value;
50
+ return {
51
+ summary: typeof source.summary === "string"
52
+ ? source.summary.slice(0, 600)
53
+ : "bounded view unavailable",
54
+ ...(typeof source.health === "string" ? { health: source.health } : {}),
55
+ ...(typeof source.projectionStatus === "string"
56
+ ? { projectionStatus: source.projectionStatus }
57
+ : {}),
58
+ ...(Array.isArray(source.findings)
59
+ ? {
60
+ findings: source.findings
61
+ .filter((item) => typeof item === "string")
62
+ .slice(0, 8)
63
+ .map((item) => item.slice(0, 240)),
64
+ }
65
+ : {}),
66
+ ...(Array.isArray(source.evidenceRefs)
67
+ ? {
68
+ evidenceRefs: source.evidenceRefs
69
+ .filter((item) => typeof item === "string")
70
+ .slice(0, 8),
71
+ }
72
+ : {}),
73
+ };
74
+ }
75
+ function compactAssessment(value) {
76
+ return {
77
+ ...value,
78
+ findings: value.findings.slice(0, 8),
79
+ risks: value.risks.slice(0, 8),
80
+ anomalies: value.anomalies.slice(0, 8),
81
+ hypotheses: value.hypotheses.slice(0, 8),
82
+ unresolved: value.unresolved.slice(0, 8),
83
+ needsDrilldown: value.needsDrilldown.slice(0, 8),
84
+ evidenceRefs: value.evidenceRefs.slice(0, 8),
85
+ carryForward: value.carryForward.slice(0, 8),
86
+ rationale: value.rationale.slice(0, 240),
87
+ };
88
+ }
89
+ export function createSystemObserver(options) {
90
+ const priority = "LOWEST";
91
+ const now = options.now ?? (() => new Date());
92
+ const idFactory = options.idFactory ?? (() => crypto.randomUUID());
93
+ const readViews = async (views) => {
94
+ const entries = await Promise.all(views.map(async (view) => [view, await options.snapshots.readView(view)]));
95
+ return Object.fromEntries(entries);
96
+ };
97
+ const synthesize = async (input) => {
98
+ const observedAt = now().toISOString();
99
+ const assessmentRef = `assessment:${idFactory()}`;
100
+ const previousUnresolved = [...(input?.previousUnresolved ?? [])];
101
+ const previousCarryForward = (input?.previousCarryForward ?? []).map((item) => ({ ...item }));
102
+ if (!options.reason) {
103
+ return {
104
+ assessmentRef,
105
+ priority: "DEFERRED",
106
+ observedAt,
107
+ assessments: [],
108
+ drilldown: [],
109
+ global: null,
110
+ status: "DEFERRED",
111
+ errorCode: "REASON_UNAVAILABLE",
112
+ };
113
+ }
114
+ try {
115
+ const assessments = [];
116
+ for (const batch of DEFAULT_CONCERN_BATCHES) {
117
+ const views = await readViews(batch.views);
118
+ const result = await options.reason({
119
+ assessmentRef,
120
+ kind: "CONCERN_BATCH",
121
+ scope: batch.scope,
122
+ observedAt,
123
+ views,
124
+ previousUnresolved,
125
+ previousCarryForward,
126
+ });
127
+ if (isReasonFailure(result) &&
128
+ result.errorCode === "CONTEXT_TOO_LARGE" &&
129
+ batch.views.length > 1) {
130
+ for (const view of batch.views) {
131
+ const singleViews = await readViews([view]);
132
+ let single = await options.reason({
133
+ assessmentRef,
134
+ kind: "CONCERN_BATCH",
135
+ scope: `${batch.scope}:${view}`,
136
+ observedAt,
137
+ views: singleViews,
138
+ previousUnresolved,
139
+ previousCarryForward,
140
+ });
141
+ if (isReasonFailure(single) &&
142
+ single.errorCode === "CONTEXT_TOO_LARGE") {
143
+ single = await options.reason({
144
+ assessmentRef,
145
+ kind: "CONCERN_BATCH",
146
+ scope: `${batch.scope}:${view}:compact`,
147
+ observedAt,
148
+ views: Object.fromEntries(Object.entries(singleViews).map(([key, value]) => [
149
+ key,
150
+ compactView(value),
151
+ ])),
152
+ previousUnresolved: previousUnresolved.slice(0, 8),
153
+ previousCarryForward: previousCarryForward.slice(0, 8),
154
+ });
155
+ }
156
+ if (isReasonFailure(single))
157
+ throw Object.assign(new Error(single.errorCode), {
158
+ code: single.errorCode,
159
+ });
160
+ assessments.push(normalizeAssessment(single, `${batch.scope}:${view}`, observedAt));
161
+ }
162
+ continue;
163
+ }
164
+ if (isReasonFailure(result))
165
+ throw Object.assign(new Error(result.errorCode), {
166
+ code: result.errorCode,
167
+ });
168
+ assessments.push(normalizeAssessment(result, batch.scope, observedAt));
169
+ }
170
+ const topics = [
171
+ ...new Set(assessments.flatMap((item) => item.needsDrilldown)),
172
+ ];
173
+ const drilldown = [];
174
+ if (options.snapshots.readDrilldown) {
175
+ for (const topic of topics) {
176
+ drilldown.push({
177
+ topic,
178
+ data: await options.snapshots.readDrilldown({
179
+ topic,
180
+ views: SYSTEM_OBSERVER_VIEWS,
181
+ }),
182
+ });
183
+ }
184
+ }
185
+ const topLevelViews = await readViews(SYSTEM_OBSERVER_VIEWS);
186
+ let globalResult = await options.reason({
187
+ assessmentRef,
188
+ kind: "GLOBAL_SYNTHESIS",
189
+ scope: "global",
190
+ observedAt,
191
+ views: topLevelViews,
192
+ previousUnresolved,
193
+ previousCarryForward,
194
+ batchAssessments: assessments,
195
+ drilldown,
196
+ });
197
+ if (isReasonFailure(globalResult) &&
198
+ globalResult.errorCode === "CONTEXT_TOO_LARGE") {
199
+ globalResult = await options.reason({
200
+ assessmentRef,
201
+ kind: "GLOBAL_SYNTHESIS",
202
+ scope: "global:compact",
203
+ observedAt,
204
+ views: Object.fromEntries(Object.entries(topLevelViews).map(([key, value]) => [
205
+ key,
206
+ compactView(value),
207
+ ])),
208
+ previousUnresolved: previousUnresolved.slice(0, 8),
209
+ previousCarryForward: previousCarryForward.slice(0, 8),
210
+ batchAssessments: assessments.slice(0, 8).map(compactAssessment),
211
+ drilldown: drilldown.slice(0, 8).map((item) => ({
212
+ topic: item.topic.slice(0, 240),
213
+ data: compactView(item.data),
214
+ })),
215
+ });
216
+ }
217
+ if (isReasonFailure(globalResult))
218
+ throw Object.assign(new Error(globalResult.errorCode), {
219
+ code: globalResult.errorCode,
220
+ });
221
+ const global = normalizeAssessment(globalResult, "global", observedAt);
222
+ return {
223
+ assessmentRef,
224
+ priority,
225
+ observedAt,
226
+ assessments,
227
+ drilldown,
228
+ global,
229
+ status: "ASSESSED",
230
+ };
231
+ }
232
+ catch (error) {
233
+ const errorCode = typeof error === "object" && error !== null
234
+ ? Reflect.get(error, "code")
235
+ : undefined;
236
+ const code = errorCode === "CONTEXT_TOO_LARGE" || errorCode === "REASON_UNAVAILABLE"
237
+ ? errorCode
238
+ : "REASON_FAILED";
239
+ return {
240
+ assessmentRef,
241
+ priority: "DEFERRED",
242
+ observedAt,
243
+ assessments: [],
244
+ drilldown: [],
245
+ global: null,
246
+ status: "DEFERRED",
247
+ errorCode: code,
248
+ };
249
+ }
250
+ };
251
+ return Object.freeze({ synthesize });
252
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Deterministic Task Observer.
3
+ *
4
+ * The Task Observer is a progression detector in the Extension application
5
+ * layer. It reads the bounded read-only drive projection exposed by the Task
6
+ * owner and, on a deterministic condition, requests a typed carrier action
7
+ * such as WAKE or RESUME. It never writes Task or Node state itself; the
8
+ * Worker performs formal work acceptance after being woken.
9
+ */
10
+ export type TaskDriveProjection = {
11
+ taskId: string;
12
+ taskStatus: string;
13
+ taskVersion: number;
14
+ terminal: boolean;
15
+ currentNode: {
16
+ nodeId: string;
17
+ status: string;
18
+ version: number;
19
+ runNo: number;
20
+ requiredAgentPackageRef: string;
21
+ } | null;
22
+ roleBinding: {
23
+ agentPackageRef: string;
24
+ roleRef: string;
25
+ workerRef: string | null;
26
+ conversationLocator: string | null;
27
+ } | null;
28
+ canDrive: boolean;
29
+ blockedReason: string | null;
30
+ };
31
+ export type TaskObserverResumeSignal = {
32
+ trigger: "EXECUTION_RESULT_READY" | "PEER_REPLY_READY" | "RECOVERY_RESUME";
33
+ ref: string;
34
+ targetWorkerRef: string;
35
+ };
36
+ export type TaskObserverAnomalySignal = {
37
+ kind: "FACT_CONFLICT" | "UNKNOWN_REALITY" | "STALLED" | "RECOVERY_FAILED";
38
+ ref: string;
39
+ facts: Record<string, string | number | boolean | null>;
40
+ };
41
+ export type TaskObserverDiagnosticAssessment = {
42
+ finding: string;
43
+ probableCause: string;
44
+ confidence: number;
45
+ recommendedNextObservation: string;
46
+ recommendedRecoveryAction: string;
47
+ needsHumanAttention: boolean;
48
+ };
49
+ export type TaskObserverDiagnosticFailure = {
50
+ ok: false;
51
+ errorCode: "REASON_UNAVAILABLE" | "CONTEXT_TOO_LARGE" | "REASON_FAILED";
52
+ };
53
+ export interface TaskObserverDiagnosticPort {
54
+ assess(input: {
55
+ taskId: string;
56
+ nodeId: string;
57
+ runNo: number;
58
+ anomaly: TaskObserverAnomalySignal;
59
+ }): Promise<TaskObserverDiagnosticAssessment | TaskObserverDiagnosticFailure>;
60
+ }
61
+ export interface TaskObserverOwnerPort {
62
+ getTaskDriveProjection(taskId: string): Promise<TaskDriveProjection>;
63
+ }
64
+ export interface TaskObserverCarrierPort {
65
+ requestWake(input: {
66
+ taskId: string;
67
+ nodeId: string;
68
+ runNo: number;
69
+ roleRef: string;
70
+ workerRef: string;
71
+ trigger: string;
72
+ conversationLocator: string | null;
73
+ underlyingRef?: string;
74
+ }): Promise<unknown>;
75
+ }
76
+ export type TaskObserverDecision = {
77
+ kind: "WAKE";
78
+ taskId: string;
79
+ nodeId: string;
80
+ runNo: number;
81
+ roleRef: string;
82
+ workerRef: string;
83
+ trigger: string;
84
+ conversationLocator: string | null;
85
+ } | {
86
+ kind: "RESUME";
87
+ taskId: string;
88
+ nodeId: string;
89
+ runNo: number;
90
+ roleRef: string;
91
+ workerRef: string;
92
+ trigger: string;
93
+ conversationLocator: string;
94
+ underlyingRef: string;
95
+ } | {
96
+ kind: "DIAGNOSTIC";
97
+ taskId: string;
98
+ nodeId: string;
99
+ runNo: number;
100
+ anomalyRef: string;
101
+ assessment: TaskObserverDiagnosticAssessment;
102
+ } | {
103
+ kind: "STOP_DRIVING";
104
+ taskId: string;
105
+ reason: "TERMINAL";
106
+ } | {
107
+ kind: "NOOP";
108
+ taskId: string;
109
+ reason: string;
110
+ };
111
+ export declare function createTaskObserver(options: {
112
+ owner: TaskObserverOwnerPort;
113
+ carrier: TaskObserverCarrierPort;
114
+ diagnostic?: TaskObserverDiagnosticPort;
115
+ }): Readonly<{
116
+ advance: (taskId: string, resumeSignal?: TaskObserverResumeSignal, anomalySignal?: TaskObserverAnomalySignal) => Promise<TaskObserverDecision>;
117
+ drive: (taskId: string, resumeSignal?: TaskObserverResumeSignal, anomalySignal?: TaskObserverAnomalySignal) => Promise<TaskObserverDecision>;
118
+ }>;