gestalt-mobile 0.17.2 → 0.18.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/README.md +5 -1
- package/dist/client/assets/index-B0Uq4OB0.css +1 -0
- package/dist/client/assets/index-orfZBEaD.js +27 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +6 -0
- package/dist/server/server/composition.js +413 -21
- package/dist/server/server/features/agent-activity/model.js +197 -0
- package/dist/server/server/features/agent-activity/registry.js +155 -0
- package/dist/server/server/features/autopilot/application/policy.js +55 -0
- package/dist/server/server/features/autopilot/application/ports.js +6 -0
- package/dist/server/server/features/autopilot/application/service.js +542 -0
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +40 -0
- package/dist/server/server/features/autopilot/register-routes.js +9 -0
- package/dist/server/server/features/autopilot/toggle/endpoint.js +32 -0
- package/dist/server/server/features/org-plan-attention/application/ports.js +6 -0
- package/dist/server/server/features/org-plan-attention/get-active/endpoint.js +22 -0
- package/dist/server/server/features/org-plan-attention/register-routes.js +11 -0
- package/dist/server/server/features/org-plan-attention/resolve/endpoint.js +42 -0
- package/dist/server/server/features/plans/application/parse-supervised-plan.js +4 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +62 -2
- package/dist/server/server/features/sessions/get-history/history-mapper.js +17 -5
- package/dist/server/server/features/sessions/get-session/endpoint.js +3 -1
- package/dist/server/server/features/sessions/interaction/kind.js +1 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +5 -1
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +2 -0
- package/dist/server/server/features/sessions/model/relay-session.js +3 -0
- package/dist/server/server/features/sessions/refresh-activity/endpoint.js +14 -0
- package/dist/server/server/features/sessions/register-routes.js +15 -2
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +15 -2
- package/dist/server/server/features/sessions/restore-session/endpoint.js +21 -5
- package/dist/server/server/features/skills/list-available/endpoint.js +3 -4
- package/dist/server/server/features/skills/model/skill-profile.js +13 -5
- package/dist/server/server/platform/codex/activity-facts.js +88 -0
- package/dist/server/server/platform/codex/server-request.js +21 -4
- package/dist/server/server/platform/codex/session-runtime.js +114 -22
- package/dist/server/server/platform/persistence/migrate.js +16 -1
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +151 -0
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +92 -4
- package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +62 -3
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +5 -2
- package/dist/server/shared/contracts/org-plan-attention.js +101 -0
- package/package.json +1 -1
- package/dist/client/assets/index-DgD-0I3M.js +0 -25
- package/dist/client/assets/index-e2lJ5XqL.css +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
const states = [
|
|
7
|
+
'blocked',
|
|
8
|
+
'awaitingHuman',
|
|
9
|
+
'working',
|
|
10
|
+
'awaitingAgent',
|
|
11
|
+
'idle',
|
|
12
|
+
'disconnected',
|
|
13
|
+
];
|
|
14
|
+
export function createAgentActivitySnapshot(sessionId, observedAt) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
sessionId,
|
|
17
|
+
root: Object.freeze({
|
|
18
|
+
state: 'disconnected',
|
|
19
|
+
reason: 'unknown',
|
|
20
|
+
observedAt,
|
|
21
|
+
lastActivityAt: observedAt,
|
|
22
|
+
}),
|
|
23
|
+
subagents: Object.freeze([]),
|
|
24
|
+
aggregateSubagents: 'idle',
|
|
25
|
+
confidence: 'stale',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Pure reducer: authoritative facts win; an observed heartbeat never makes an active actor idle. */
|
|
29
|
+
export function projectAgentActivity(current, fact) {
|
|
30
|
+
if (fact.sessionId !== current.sessionId || !validTimestamp(fact.occurredAt))
|
|
31
|
+
return current;
|
|
32
|
+
// Notifications can replay or arrive out of order. Never let an older fact
|
|
33
|
+
// regress an actor's authoritative state.
|
|
34
|
+
if (Date.parse(fact.occurredAt) < Date.parse(current.root.observedAt))
|
|
35
|
+
return current;
|
|
36
|
+
// A shared app-server stream includes spawned threads. Only a known root
|
|
37
|
+
// thread may change root state; child-thread facts are mapped to that child.
|
|
38
|
+
if (fact.threadId && current.rootThreadId && fact.threadId !== current.rootThreadId) {
|
|
39
|
+
const child = current.subagents.find((candidate) => candidate.threadId === fact.threadId);
|
|
40
|
+
if (!child)
|
|
41
|
+
return current;
|
|
42
|
+
const childFact = { ...fact };
|
|
43
|
+
delete childFact.threadId;
|
|
44
|
+
return projectAgentActivity(current, {
|
|
45
|
+
...childFact,
|
|
46
|
+
childId: child.id,
|
|
47
|
+
kind: 'collaboration',
|
|
48
|
+
childStatus: fact.status,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const observedAt = later(current.root.observedAt, fact.occurredAt);
|
|
52
|
+
const root = {
|
|
53
|
+
...current.root,
|
|
54
|
+
observedAt,
|
|
55
|
+
lastActivityAt: later(current.root.lastActivityAt, fact.occurredAt),
|
|
56
|
+
};
|
|
57
|
+
const children = new Map(current.subagents.map((child) => [child.id, child]));
|
|
58
|
+
if (fact.kind === 'turnStarted')
|
|
59
|
+
Object.assign(root, { state: 'working', reason: 'turnActive' });
|
|
60
|
+
if (fact.kind === 'threadStarted')
|
|
61
|
+
applyStatus(root, fact.status);
|
|
62
|
+
if (fact.kind === 'turnCompleted' && root.state !== 'awaitingHuman')
|
|
63
|
+
Object.assign(root, { state: 'idle', reason: 'turnCompleted' });
|
|
64
|
+
if (fact.kind === 'interactionPending')
|
|
65
|
+
Object.assign(root, {
|
|
66
|
+
state: 'awaitingHuman',
|
|
67
|
+
reason: fact.attentionReason ?? 'pendingInteraction',
|
|
68
|
+
});
|
|
69
|
+
if (fact.kind === 'interactionResolved' && root.state === 'awaitingHuman')
|
|
70
|
+
Object.assign(root, fact.hasPendingInteraction
|
|
71
|
+
? { state: 'awaitingHuman', reason: fact.attentionReason ?? 'pendingInteraction' }
|
|
72
|
+
: { state: 'working', reason: 'turnActive' });
|
|
73
|
+
if (fact.kind === 'processExited')
|
|
74
|
+
Object.assign(root, { state: 'disconnected', reason: 'processExited' });
|
|
75
|
+
if (fact.kind === 'threadStatus')
|
|
76
|
+
applyStatus(root, fact.status);
|
|
77
|
+
if (fact.kind === 'collaboration') {
|
|
78
|
+
// Older servers may omit experimental child metadata. That is a capability
|
|
79
|
+
// downgrade, not evidence that the root process disappeared.
|
|
80
|
+
if (!fact.childId)
|
|
81
|
+
root.reason = 'missingCollaborationMetadata';
|
|
82
|
+
else {
|
|
83
|
+
const before = children.get(fact.childId);
|
|
84
|
+
const child = Object.freeze({
|
|
85
|
+
id: fact.childId,
|
|
86
|
+
...(fact.childThreadId
|
|
87
|
+
? { threadId: fact.childThreadId }
|
|
88
|
+
: before?.threadId
|
|
89
|
+
? { threadId: before.threadId }
|
|
90
|
+
: {}),
|
|
91
|
+
...(fact.childNickname
|
|
92
|
+
? { nickname: fact.childNickname }
|
|
93
|
+
: before?.nickname
|
|
94
|
+
? { nickname: before.nickname }
|
|
95
|
+
: {}),
|
|
96
|
+
...(fact.childRole ? { role: fact.childRole } : before?.role ? { role: before.role } : {}),
|
|
97
|
+
state: childState(fact.childStatus, fact.collaborationAction, before?.state),
|
|
98
|
+
reason: childReason(fact.childStatus, fact.collaborationAction),
|
|
99
|
+
observedAt: fact.occurredAt,
|
|
100
|
+
lastActivityAt: later(before?.lastActivityAt ?? fact.occurredAt, fact.occurredAt),
|
|
101
|
+
});
|
|
102
|
+
children.set(fact.childId, child);
|
|
103
|
+
if (fact.collaborationAction === 'wait' && root.state === 'working')
|
|
104
|
+
Object.assign(root, { state: 'awaitingAgent', reason: 'collaborationWait' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const subagents = Object.freeze([...children.values()].sort((a, b) => a.id.localeCompare(b.id)));
|
|
108
|
+
const next = Object.freeze({
|
|
109
|
+
sessionId: current.sessionId,
|
|
110
|
+
...(current.rootThreadId || (fact.kind === 'threadStarted' && fact.threadId)
|
|
111
|
+
? { rootThreadId: current.rootThreadId ?? fact.threadId }
|
|
112
|
+
: {}),
|
|
113
|
+
root: Object.freeze(root),
|
|
114
|
+
subagents,
|
|
115
|
+
aggregateSubagents: aggregate(subagents),
|
|
116
|
+
confidence: current.confidence,
|
|
117
|
+
});
|
|
118
|
+
return same(current, next) ? current : next;
|
|
119
|
+
}
|
|
120
|
+
export function withActivityConfidence(snapshot, confidence) {
|
|
121
|
+
return snapshot.confidence === confidence ? snapshot : Object.freeze({ ...snapshot, confidence });
|
|
122
|
+
}
|
|
123
|
+
/** Process loss revokes child identities; recovery must repopulate them from a fresh list read. */
|
|
124
|
+
export function clearAgentActivityChildren(snapshot) {
|
|
125
|
+
if (snapshot.subagents.length === 0 && snapshot.aggregateSubagents === 'idle')
|
|
126
|
+
return snapshot;
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
...snapshot,
|
|
129
|
+
subagents: Object.freeze([]),
|
|
130
|
+
aggregateSubagents: 'idle',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function applyStatus(root, status) {
|
|
134
|
+
if (status === 'error' || status === 'systemError' || status === 'failed')
|
|
135
|
+
Object.assign(root, { state: 'blocked', reason: 'agentError' });
|
|
136
|
+
else if (status === 'notLoaded')
|
|
137
|
+
Object.assign(root, { state: 'disconnected', reason: 'processExited' });
|
|
138
|
+
else if (status === 'idle' || status === 'completed')
|
|
139
|
+
Object.assign(root, { state: 'idle', reason: 'turnCompleted' });
|
|
140
|
+
else if (status === 'active' || status === 'working')
|
|
141
|
+
Object.assign(root, { state: 'working', reason: 'turnActive' });
|
|
142
|
+
}
|
|
143
|
+
function childState(status, action, previous = 'working') {
|
|
144
|
+
if (status === 'error' || status === 'failed' || status === 'systemError')
|
|
145
|
+
return 'blocked';
|
|
146
|
+
if (status === 'completed' || status === 'idle' || action === 'close_agent')
|
|
147
|
+
return 'idle';
|
|
148
|
+
// `wait` belongs to the caller/root; it does not rewrite the child which may
|
|
149
|
+
// still be working. A child can explicitly report its own waiting status.
|
|
150
|
+
if (status === 'disconnected' || status === 'notLoaded')
|
|
151
|
+
return 'disconnected';
|
|
152
|
+
return previous === 'idle' && action === 'resume_agent'
|
|
153
|
+
? 'working'
|
|
154
|
+
: status === 'working' || status === 'active'
|
|
155
|
+
? 'working'
|
|
156
|
+
: previous;
|
|
157
|
+
}
|
|
158
|
+
function childReason(status, action) {
|
|
159
|
+
if (status === 'error' || status === 'failed' || status === 'systemError')
|
|
160
|
+
return 'agentError';
|
|
161
|
+
if (status === 'disconnected' || status === 'notLoaded')
|
|
162
|
+
return 'processExited';
|
|
163
|
+
if (action === 'wait')
|
|
164
|
+
return 'collaborationWait';
|
|
165
|
+
if (status === 'completed' || status === 'idle' || action === 'close_agent')
|
|
166
|
+
return 'turnCompleted';
|
|
167
|
+
return 'turnActive';
|
|
168
|
+
}
|
|
169
|
+
function aggregate(children) {
|
|
170
|
+
return states.find((state) => children.some((child) => child.state === state)) ?? 'idle';
|
|
171
|
+
}
|
|
172
|
+
function validTimestamp(value) {
|
|
173
|
+
return !Number.isNaN(Date.parse(value));
|
|
174
|
+
}
|
|
175
|
+
function later(left, right) {
|
|
176
|
+
return Date.parse(right) > Date.parse(left) ? right : left;
|
|
177
|
+
}
|
|
178
|
+
function same(left, right) {
|
|
179
|
+
return JSON.stringify(semantic(left)) === JSON.stringify(semantic(right));
|
|
180
|
+
}
|
|
181
|
+
function semantic(snapshot) {
|
|
182
|
+
return {
|
|
183
|
+
sessionId: snapshot.sessionId,
|
|
184
|
+
rootThreadId: snapshot.rootThreadId,
|
|
185
|
+
confidence: snapshot.confidence,
|
|
186
|
+
root: { state: snapshot.root.state, reason: snapshot.root.reason },
|
|
187
|
+
subagents: snapshot.subagents.map((child) => ({
|
|
188
|
+
id: child.id,
|
|
189
|
+
threadId: child.threadId,
|
|
190
|
+
nickname: child.nickname,
|
|
191
|
+
role: child.role,
|
|
192
|
+
state: child.state,
|
|
193
|
+
reason: child.reason,
|
|
194
|
+
})),
|
|
195
|
+
aggregateSubagents: snapshot.aggregateSubagents,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { createAgentActivitySnapshot, clearAgentActivityChildren, projectAgentActivity, withActivityConfidence, } from './model.js';
|
|
7
|
+
/** Session-local read model. Scheduling and reconciliation are injected ports. */
|
|
8
|
+
export class AgentActivityRegistry {
|
|
9
|
+
publish;
|
|
10
|
+
options;
|
|
11
|
+
#snapshots = new Map();
|
|
12
|
+
#timers = new Map();
|
|
13
|
+
#generation = new Map();
|
|
14
|
+
#inFlight = new Map();
|
|
15
|
+
constructor(publish, options = {}) {
|
|
16
|
+
this.publish = publish;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
snapshot(sessionId, now) {
|
|
20
|
+
return this.#snapshots.get(sessionId) ?? createAgentActivitySnapshot(sessionId, now);
|
|
21
|
+
}
|
|
22
|
+
observe(fact) {
|
|
23
|
+
const current = this.snapshot(fact.sessionId, fact.occurredAt);
|
|
24
|
+
const next = withActivityConfidence(projectAgentActivity(current, fact), fact.kind === 'collaboration' && !fact.childId ? 'stale' : 'fresh');
|
|
25
|
+
if (next !== current) {
|
|
26
|
+
this.#snapshots.set(fact.sessionId, next);
|
|
27
|
+
this.publish(next, fact.occurredAt);
|
|
28
|
+
}
|
|
29
|
+
this.#generation.set(fact.sessionId, (this.#generation.get(fact.sessionId) ?? 0) + 1);
|
|
30
|
+
this.#armStaleness(fact.sessionId);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
reconciling(sessionId, occurredAt) {
|
|
34
|
+
return this.#qualify(sessionId, occurredAt, 'reconciling');
|
|
35
|
+
}
|
|
36
|
+
disconnected(sessionId, occurredAt) {
|
|
37
|
+
this.suspend(sessionId);
|
|
38
|
+
const current = this.snapshot(sessionId, occurredAt);
|
|
39
|
+
const next = withActivityConfidence(clearAgentActivityChildren(projectAgentActivity(current, { sessionId, occurredAt, kind: 'processExited' })), 'stale');
|
|
40
|
+
if (next !== current) {
|
|
41
|
+
this.#snapshots.set(sessionId, next);
|
|
42
|
+
this.publish(next, occurredAt);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
suspend(sessionId) {
|
|
47
|
+
this.#timers.get(sessionId)?.();
|
|
48
|
+
this.#timers.delete(sessionId);
|
|
49
|
+
this.#generation.set(sessionId, (this.#generation.get(sessionId) ?? 0) + 1);
|
|
50
|
+
// The RPC cannot be cancelled, but a restart must not coalesce onto its
|
|
51
|
+
// obsolete promise. Generation guards suppress its eventual result.
|
|
52
|
+
this.#inFlight.delete(sessionId);
|
|
53
|
+
}
|
|
54
|
+
reconciled(sessionId, occurredAt) {
|
|
55
|
+
return this.#qualify(sessionId, occurredAt, 'fresh');
|
|
56
|
+
}
|
|
57
|
+
refresh(sessionId) {
|
|
58
|
+
if (!this.options.reconcile)
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
const existing = this.#inFlight.get(sessionId);
|
|
61
|
+
if (existing)
|
|
62
|
+
return existing;
|
|
63
|
+
const generation = (this.#generation.get(sessionId) ?? 0) + 1;
|
|
64
|
+
this.#generation.set(sessionId, generation);
|
|
65
|
+
const running = this.#attempt(sessionId, generation, 1).finally(() => {
|
|
66
|
+
if (this.#inFlight.get(sessionId) === running)
|
|
67
|
+
this.#inFlight.delete(sessionId);
|
|
68
|
+
});
|
|
69
|
+
this.#inFlight.set(sessionId, running);
|
|
70
|
+
return running;
|
|
71
|
+
}
|
|
72
|
+
/** Replaces the observed direct-child set; absent children are disconnected. */
|
|
73
|
+
childrenReconciled(sessionId, occurredAt, children) {
|
|
74
|
+
let next = this.snapshot(sessionId, occurredAt);
|
|
75
|
+
const seen = new Set(children.map((child) => child.id));
|
|
76
|
+
for (const child of children)
|
|
77
|
+
next = projectAgentActivity(next, {
|
|
78
|
+
sessionId,
|
|
79
|
+
occurredAt,
|
|
80
|
+
kind: 'collaboration',
|
|
81
|
+
childId: child.id,
|
|
82
|
+
childThreadId: child.id,
|
|
83
|
+
...(child.status ? { childStatus: child.status } : {}),
|
|
84
|
+
...(child.nickname ? { childNickname: child.nickname } : {}),
|
|
85
|
+
...(child.role ? { childRole: child.role } : {}),
|
|
86
|
+
});
|
|
87
|
+
for (const child of next.subagents)
|
|
88
|
+
if (!seen.has(child.id))
|
|
89
|
+
next = projectAgentActivity(next, {
|
|
90
|
+
sessionId,
|
|
91
|
+
occurredAt,
|
|
92
|
+
kind: 'collaboration',
|
|
93
|
+
childId: child.id,
|
|
94
|
+
childStatus: 'notLoaded',
|
|
95
|
+
});
|
|
96
|
+
// A malformed/unknown `thread/list` row is not proof that a child is
|
|
97
|
+
// healthy. Preserve it as disconnected but keep the aggregate qualified
|
|
98
|
+
// only as stale until a later authoritative read succeeds.
|
|
99
|
+
next = withActivityConfidence(next, children.some((child) => child.qualified === false) ? 'stale' : 'fresh');
|
|
100
|
+
const current = this.snapshot(sessionId, occurredAt);
|
|
101
|
+
if (next !== current) {
|
|
102
|
+
this.#snapshots.set(sessionId, next);
|
|
103
|
+
this.publish(next, occurredAt);
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
dispose(sessionId) {
|
|
108
|
+
this.#snapshots.delete(sessionId);
|
|
109
|
+
this.#timers.get(sessionId)?.();
|
|
110
|
+
this.#timers.delete(sessionId);
|
|
111
|
+
this.#generation.delete(sessionId);
|
|
112
|
+
this.#inFlight.delete(sessionId);
|
|
113
|
+
}
|
|
114
|
+
#armStaleness(sessionId) {
|
|
115
|
+
if (!this.options.schedule || !this.options.reconcile)
|
|
116
|
+
return;
|
|
117
|
+
this.#timers.get(sessionId)?.();
|
|
118
|
+
this.#timers.set(sessionId, this.options.schedule(() => void this.refresh(sessionId), this.options.staleAfterMs ?? 30_000));
|
|
119
|
+
}
|
|
120
|
+
async #attempt(sessionId, generation, attempt) {
|
|
121
|
+
if (this.#generation.get(sessionId) !== generation)
|
|
122
|
+
return;
|
|
123
|
+
this.#timers.delete(sessionId);
|
|
124
|
+
const now = this.options.now?.() ?? new Date().toISOString();
|
|
125
|
+
this.reconciling(sessionId, now);
|
|
126
|
+
try {
|
|
127
|
+
await this.options.reconcile?.(sessionId);
|
|
128
|
+
if (this.#generation.get(sessionId) === generation)
|
|
129
|
+
this.reconciled(sessionId, this.options.now?.() ?? now);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
if (this.#generation.get(sessionId) !== generation)
|
|
133
|
+
return;
|
|
134
|
+
const delays = this.options.retryDelaysMs ?? [1_000, 5_000, 15_000];
|
|
135
|
+
if (attempt >=
|
|
136
|
+
Math.min(this.options.maxReconcileAttempts ?? delays.length + 1, delays.length + 1)) {
|
|
137
|
+
this.options.diagnostic?.(sessionId, 'reconcileExhausted');
|
|
138
|
+
this.disconnected(sessionId, this.options.now?.() ?? now);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#timers.set(sessionId, this.options.schedule(() => void this.#attempt(sessionId, generation, attempt + 1), delays[attempt - 1]));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
#qualify(sessionId, occurredAt, confidence, fact) {
|
|
145
|
+
const current = this.snapshot(sessionId, occurredAt);
|
|
146
|
+
const next = fact
|
|
147
|
+
? withActivityConfidence(projectAgentActivity(current, { sessionId, occurredAt, ...fact }), confidence)
|
|
148
|
+
: withActivityConfidence(current, confidence);
|
|
149
|
+
if (next !== current) {
|
|
150
|
+
this.#snapshots.set(sessionId, next);
|
|
151
|
+
this.publish(next, occurredAt);
|
|
152
|
+
}
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v1';
|
|
7
|
+
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Invoke gestalt_org_plan_attention only for a decision-table blocker; otherwise immediately perform the next legal lifecycle action. Do not send a status-only response.';
|
|
8
|
+
export const defaultAutopilotPolicy = Object.freeze({
|
|
9
|
+
quiescenceMs: 1_000,
|
|
10
|
+
staleAfterMs: 30_000,
|
|
11
|
+
retryLimit: 3,
|
|
12
|
+
backoffMs: (attempt) => Math.min(60_000, 1_000 * 2 ** Math.max(0, attempt)),
|
|
13
|
+
promptVersion: AUTOPILOT_PROMPT_VERSION,
|
|
14
|
+
});
|
|
15
|
+
export function executionComplete(plan) {
|
|
16
|
+
return (plan.executionComplete ??
|
|
17
|
+
plan.steps.every((step) => step.state === 'DONE' &&
|
|
18
|
+
step.reviewStatus === 'REVIEWED' &&
|
|
19
|
+
step.children.every((child) => child.state === 'DONE')));
|
|
20
|
+
}
|
|
21
|
+
/** A deliberately pure, exhaustive safety gate. Adapters may only enact this result. */
|
|
22
|
+
export function decideAutopilot(input) {
|
|
23
|
+
const { state, plan, activity, hasPendingInteraction, now, policy } = input;
|
|
24
|
+
if (!state.requestedEnabled)
|
|
25
|
+
return { kind: 'disable', reason: 'manualDisabled' };
|
|
26
|
+
if (!plan)
|
|
27
|
+
return { kind: 'disable', reason: 'planRequired' };
|
|
28
|
+
if (executionComplete(plan))
|
|
29
|
+
return { kind: 'complete' };
|
|
30
|
+
if (hasPendingInteraction || input.hasActiveAttention || state.state === 'attentionRequired')
|
|
31
|
+
return { kind: 'requestAttention', reason: 'attentionRequired' };
|
|
32
|
+
if ((input.planIdentity && state.planIdentity && input.planIdentity !== state.planIdentity) ||
|
|
33
|
+
(input.planFingerprint &&
|
|
34
|
+
state.planFingerprint &&
|
|
35
|
+
input.planFingerprint !== state.planFingerprint))
|
|
36
|
+
return { kind: 'observe' };
|
|
37
|
+
if (!activity || activity.confidence !== 'fresh')
|
|
38
|
+
return { kind: 'reconcile' };
|
|
39
|
+
if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
|
|
40
|
+
return { kind: 'reconcile' };
|
|
41
|
+
if (activity.root.state !== 'idle' || activity.aggregateSubagents !== 'idle')
|
|
42
|
+
return activity.aggregateSubagents === 'disconnected'
|
|
43
|
+
? { kind: 'reconcile' }
|
|
44
|
+
: { kind: 'observe' };
|
|
45
|
+
// The outcome is intentionally interpreted only through durable lack of plan
|
|
46
|
+
// progress: a failed or unknown automatic turn may retry within the same
|
|
47
|
+
// bounded budget, while a completed turn with no fingerprint change does too.
|
|
48
|
+
if (state.consecutiveNoProgress >= policy.retryLimit &&
|
|
49
|
+
['completed', 'failed', 'unknown', undefined].includes(input.lastTurnOutcome))
|
|
50
|
+
return { kind: 'requestAttention', reason: 'noPlanProgress' };
|
|
51
|
+
return {
|
|
52
|
+
kind: 'scheduleContinuation',
|
|
53
|
+
at: new Date(Date.parse(now) + policy.backoffMs(state.consecutiveNoProgress)).toISOString(),
|
|
54
|
+
};
|
|
55
|
+
}
|