@bastani/atomic 0.9.13 → 0.9.14-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/i-have-adhd/package.json +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +12 -0
- package/dist/builtin/workflows/README.md +9 -0
- package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
- package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
- package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
- package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
- package/dist/builtin/workflows/builtin/goal.ts +4 -0
- package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +4 -0
- package/dist/builtin/workflows/builtin/ralph.ts +4 -0
- package/dist/builtin/workflows/builtin/tournament.ts +4 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
- package/dist/builtin/workflows/src/authoring.d.ts +3 -0
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +13 -2
- package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
- package/dist/builtin/workflows/src/engine/run.ts +1 -0
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +766 -4
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
- package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +1 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
- package/dist/core/extensions/index.d.ts +1 -0
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +26 -0
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js +17 -1
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts +21 -0
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +34 -9
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/index-extensions.d.ts +1 -1
- package/dist/index-extensions.d.ts.map +1 -1
- package/dist/index-extensions.js +1 -1
- package/dist/index-extensions.js.map +1 -1
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
- package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
- package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +5 -1
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts +3 -0
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +14 -0
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
- package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
- package/dist/modes/interactive-engine/protocol.d.ts +1 -0
- package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
- package/dist/modes/interactive-engine/protocol.js +1 -0
- package/dist/modes/interactive-engine/protocol.js.map +1 -1
- package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
- package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-component.js +3 -2
- package/dist/modes/interactive-engine/remote-component.js.map +1 -1
- package/docs/extensions.md +27 -0
- package/docs/keybindings.md +1 -0
- package/docs/workflows.md +34 -0
- package/npm-shrinkwrap.json +29 -29
- package/package.json +2 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getDurableBackend } from "../durable/factory.js";
|
|
2
|
+
import { readWorkflowHeartbeatAnchor, recordWorkflowHeartbeatAnchor } from "../durable/workflow-heartbeat-anchor.js";
|
|
1
3
|
import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
|
|
2
4
|
import type { StageAdapters } from "../runs/foreground/stage-runner.js";
|
|
3
5
|
import type { SessionManager } from "../shared/persistence-restore.js";
|
|
@@ -11,6 +13,7 @@ import type {
|
|
|
11
13
|
WorkflowPersistencePort,
|
|
12
14
|
WorkflowRuntimeConfig,
|
|
13
15
|
} from "../shared/types.js";
|
|
16
|
+
import type { WorkflowHeartbeatIdentity } from "../shared/workflow-heartbeat-contract.js";
|
|
14
17
|
import {
|
|
15
18
|
type ConfigLoadResult,
|
|
16
19
|
loadWorkflowConfig,
|
|
@@ -35,11 +38,57 @@ import {
|
|
|
35
38
|
import type { ExtensionAPI, PiModelContext } from "./public-types.js";
|
|
36
39
|
import { createExtensionRuntime, type ExtensionRuntime } from "./runtime.js";
|
|
37
40
|
import { createStatusWriter, type StatusWriter } from "./status-writer.js";
|
|
41
|
+
import { registerWorkflowHeartbeatRenderer } from "./workflow-heartbeat-notice.js";
|
|
42
|
+
import {
|
|
43
|
+
createWorkflowHeartbeatSchedulerState,
|
|
44
|
+
installWorkflowHeartbeatScheduler,
|
|
45
|
+
isWorkflowHeartbeatTerminalRun,
|
|
46
|
+
resetWorkflowHeartbeatSchedulerState,
|
|
47
|
+
type WorkflowHeartbeatAnchorStore,
|
|
48
|
+
type WorkflowHeartbeatScheduler,
|
|
49
|
+
workflowHeartbeatConsumedIdentity,
|
|
50
|
+
workflowHeartbeatContextInvalidation,
|
|
51
|
+
} from "./workflow-heartbeat-scheduler.js";
|
|
38
52
|
import { workflowModelCatalogFromContext } from "./workflow-model-catalog.js";
|
|
39
53
|
import { makeMcpPort, makePersistencePort } from "./workflow-ports.js";
|
|
40
54
|
import { createWorkflowReloadCoordinator } from "./workflow-reload-coordinator.js";
|
|
41
55
|
import { type WorkflowReloadReport, workflowReloadDiagnostics } from "./workflow-reload-report.js";
|
|
42
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort durable cadence-anchor store for workflow heartbeats.
|
|
59
|
+
*
|
|
60
|
+
* `getDurableBackend()` throws `DbosNotReadyError` until a backend exists, and
|
|
61
|
+
* the scheduler installs at `session_start`, before any workflow has run — so
|
|
62
|
+
* both sides swallow failure and fall back to the run's own `startedAt`.
|
|
63
|
+
*/
|
|
64
|
+
function durableWorkflowHeartbeatAnchorStore(): WorkflowHeartbeatAnchorStore {
|
|
65
|
+
return {
|
|
66
|
+
readAnchorAt(runId) {
|
|
67
|
+
try {
|
|
68
|
+
return readWorkflowHeartbeatAnchor(getDurableBackend(), runId);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async recordAnchorAt(runId, record) {
|
|
74
|
+
try {
|
|
75
|
+
return await recordWorkflowHeartbeatAnchor(getDurableBackend(), {
|
|
76
|
+
runId,
|
|
77
|
+
anchorAt: record.anchorAt,
|
|
78
|
+
// The scheduler only records a positive cadence; a run launched
|
|
79
|
+
// disabled never reaches this seam.
|
|
80
|
+
intervalMinutes: record.intervalMinutes ?? 0,
|
|
81
|
+
now: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
// A backend that is not ready must not break a background pass; the
|
|
85
|
+
// caller retries on the next schedule pass.
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
43
92
|
export interface WorkflowExtensionRuntimeState {
|
|
44
93
|
persistenceRef: { current: WorkflowPersistencePort | undefined };
|
|
45
94
|
mcpPort: WorkflowMcpPort | undefined;
|
|
@@ -48,6 +97,7 @@ export interface WorkflowExtensionRuntimeState {
|
|
|
48
97
|
discoveryRef: { current: DiscoveryResult | null };
|
|
49
98
|
lifecycleNotificationState: ReturnType<typeof createWorkflowLifecycleNotificationState>;
|
|
50
99
|
hilAnswerNotificationState: ReturnType<typeof createWorkflowHilAnswerNotificationState>;
|
|
100
|
+
workflowHeartbeatSchedulerState: ReturnType<typeof createWorkflowHeartbeatSchedulerState>;
|
|
51
101
|
/** Seed lifecycle notification state before completed historical snapshots are inserted. */
|
|
52
102
|
beforeRestoreCompleted(snapshots: readonly RunSnapshot[]): void;
|
|
53
103
|
runtimeForContext(ctx?: PiModelContext): ExtensionRuntime;
|
|
@@ -83,10 +133,12 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
83
133
|
let statusWriterRef: StatusWriter = createStatusWriter(store, runtimeConfigRef.current);
|
|
84
134
|
let lifecycleNotificationsUnsubscribe: (() => void) | null = null;
|
|
85
135
|
let hilAnswerNotificationsUnsubscribe: (() => void) | null = null;
|
|
136
|
+
let workflowHeartbeatScheduler: WorkflowHeartbeatScheduler | null = null;
|
|
86
137
|
let notificationsActive = false;
|
|
87
138
|
let notificationGeneration = 0;
|
|
88
139
|
const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
|
|
89
140
|
const hilAnswerNotificationState = createWorkflowHilAnswerNotificationState();
|
|
141
|
+
const workflowHeartbeatSchedulerState = createWorkflowHeartbeatSchedulerState();
|
|
90
142
|
const beforeRestoreCompleted = (snapshots: readonly RunSnapshot[]): void => {
|
|
91
143
|
seedWorkflowLifecycleNotificationState(lifecycleNotificationState, {
|
|
92
144
|
...readGraphStoreSnapshot(store),
|
|
@@ -102,6 +154,7 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
102
154
|
: undefined;
|
|
103
155
|
registerLifecycleNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
|
|
104
156
|
registerHilAnswerNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
|
|
157
|
+
registerWorkflowHeartbeatRenderer({ rendererHost: pi, registerMessageRenderer });
|
|
105
158
|
const sendWorkflowNotificationMessage: ExtensionAPI["sendMessage"] | undefined =
|
|
106
159
|
typeof pi.sendMessage === "function" ? (message, options) => pi.sendMessage!(message, options) : undefined;
|
|
107
160
|
const reinstallLifecycleNotifications = (): void => {
|
|
@@ -127,6 +180,86 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
127
180
|
sendMessage: sendWorkflowNotificationMessage,
|
|
128
181
|
});
|
|
129
182
|
};
|
|
183
|
+
// `message_end` is the host's injection signal: agent-core emits it at the
|
|
184
|
+
// moment a message enters the conversation. That is what releases a held
|
|
185
|
+
// heartbeat slot.
|
|
186
|
+
//
|
|
187
|
+
// The turn-settled event is deliberately not used. The host emits it from the
|
|
188
|
+
// prompt cycle's `finally` whether or not the queued messages were drained, so
|
|
189
|
+
// pausing the queue mid-turn would release a slot whose card is still parked
|
|
190
|
+
// and let the next boundary stack a second card behind it. `message_end` never
|
|
191
|
+
// fires for a parked card. The visible display card is published on the
|
|
192
|
+
// session-listener channel only, so an extension sees the hidden
|
|
193
|
+
// reconciliation at consumption and never the card at admission — there is no
|
|
194
|
+
// ambiguity to disambiguate. Proven end to end against a real `AgentSession`
|
|
195
|
+
// in test/unit/workflow-heartbeat-parent-pickup.test.ts.
|
|
196
|
+
//
|
|
197
|
+
// Registered exactly once, at construction: `pi.on` has no unsubscribe, so a
|
|
198
|
+
// per-install registration would accumulate a handler on every notification
|
|
199
|
+
// cycle. The mutable scheduler reference is what routes the signal to the
|
|
200
|
+
// current installation, and a `null` reference makes it a no-op.
|
|
201
|
+
//
|
|
202
|
+
// The same condition decides whether the scheduler holds a slot at all, so the
|
|
203
|
+
// registration and the option can never disagree: a host that reports no
|
|
204
|
+
// consumption would leave a held slot with nothing to release it.
|
|
205
|
+
const parentAvailabilityReported = typeof pi.on === "function";
|
|
206
|
+
if (parentAvailabilityReported) {
|
|
207
|
+
// This handler observes, and for one narrow case replaces.
|
|
208
|
+
//
|
|
209
|
+
// The last guard on the heartbeat path (issue #1975): the three before it
|
|
210
|
+
// all sit before `sendMessage`, so a heartbeat the host has already
|
|
211
|
+
// admitted is beyond them. Consumption is the final moment before its
|
|
212
|
+
// steer joins the model's context, and `message_end` may return a
|
|
213
|
+
// replacement of the same role, so a heartbeat whose run has finished,
|
|
214
|
+
// vanished, or no longer owns that exact scheduled identity is excluded
|
|
215
|
+
// from context rather than steering the parent about stale work.
|
|
216
|
+
//
|
|
217
|
+
// `absent` counts as much as `terminal`. A card recovered at the restart
|
|
218
|
+
// door is consumed just after `session_start` cleared the store, so its run
|
|
219
|
+
// is normally absent. A durable resume may instead reuse the run id, but its
|
|
220
|
+
// future pending boundary has a different `scheduledAt`. Both cases fail
|
|
221
|
+
// exact pending ownership and preserve the no-backfill rule.
|
|
222
|
+
pi.on?.("message_end", (event, ctx) => {
|
|
223
|
+
// The host hands the session manager to the handler on its context; the
|
|
224
|
+
// top-level `pi.sessionManager` is not populated in every host, so the
|
|
225
|
+
// per-call one is preferred and the other is the fallback. Verified
|
|
226
|
+
// against a real `AgentSession` in
|
|
227
|
+
// test/unit/workflow-heartbeat-parent-pickup.test.ts.
|
|
228
|
+
const entries = (ctx?.sessionManager ?? pi.sessionManager)?.getEntries?.();
|
|
229
|
+
const invalidation = workflowHeartbeatContextInvalidation(event, entries, isWorkflowHeartbeatIdentityOwned);
|
|
230
|
+
if (invalidation !== undefined) return invalidation;
|
|
231
|
+
const identity = workflowHeartbeatConsumedIdentity(event, entries);
|
|
232
|
+
if (identity !== undefined) workflowHeartbeatScheduler?.notifyHeartbeatConsumed(identity);
|
|
233
|
+
return undefined;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether this exact identity is still pending for a current nonterminal run. */
|
|
238
|
+
function isWorkflowHeartbeatIdentityOwned(identity: WorkflowHeartbeatIdentity): boolean {
|
|
239
|
+
const pending = workflowHeartbeatSchedulerState.pending.get(identity.runId);
|
|
240
|
+
const run = store.runs().find((candidate) => candidate.id === identity.runId);
|
|
241
|
+
return pending?.scheduledAt === identity.scheduledAt && run !== undefined && !isWorkflowHeartbeatTerminalRun(run);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Workflow heartbeats share the lifecycle-notice lifetime: they are armed
|
|
246
|
+
* while notifications are active and disposed with them. The cadence itself
|
|
247
|
+
* comes from the live registry, so a workflow reload is picked up without a
|
|
248
|
+
* reinstall.
|
|
249
|
+
*/
|
|
250
|
+
const reinstallWorkflowHeartbeatScheduler = (): void => {
|
|
251
|
+
workflowHeartbeatScheduler?.dispose();
|
|
252
|
+
workflowHeartbeatScheduler = null;
|
|
253
|
+
if (!notificationsActive) return;
|
|
254
|
+
workflowHeartbeatScheduler = installWorkflowHeartbeatScheduler({
|
|
255
|
+
store,
|
|
256
|
+
state: workflowHeartbeatSchedulerState,
|
|
257
|
+
sendMessage: sendWorkflowNotificationMessage,
|
|
258
|
+
resolveIntervalMinutes: (workflowName) => runtimeProxy.registry.get(workflowName)?.heartbeatIntervalMinutes,
|
|
259
|
+
parentAvailabilityReported,
|
|
260
|
+
anchorStore: durableWorkflowHeartbeatAnchorStore(),
|
|
261
|
+
});
|
|
262
|
+
};
|
|
130
263
|
|
|
131
264
|
const hostStageSessionDir: { current: string | undefined } = { current: undefined };
|
|
132
265
|
const resolveDefaultStageSessionDir = (): string | undefined => hostStageSessionDir.current;
|
|
@@ -425,6 +558,7 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
425
558
|
discoveryRef,
|
|
426
559
|
lifecycleNotificationState,
|
|
427
560
|
hilAnswerNotificationState,
|
|
561
|
+
workflowHeartbeatSchedulerState,
|
|
428
562
|
beforeRestoreCompleted,
|
|
429
563
|
runtimeForContext,
|
|
430
564
|
resetWorkflowDiscoveryForSession,
|
|
@@ -442,6 +576,10 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
442
576
|
notificationsActive = active;
|
|
443
577
|
reinstallLifecycleNotifications();
|
|
444
578
|
reinstallHilAnswerNotifications();
|
|
579
|
+
// A fresh host session restarts the cadence from each run's persisted
|
|
580
|
+
// start time; prior-session schedule and pending state cannot apply.
|
|
581
|
+
if (!active) resetWorkflowHeartbeatSchedulerState(workflowHeartbeatSchedulerState);
|
|
582
|
+
reinstallWorkflowHeartbeatScheduler();
|
|
445
583
|
},
|
|
446
584
|
updateHostStageSessionDir(sessionManager) {
|
|
447
585
|
try {
|