@pellux/goodvibes-agent 1.2.0 → 1.3.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 +159 -0
- package/dist/package/main.js +19016 -12684
- package/docs/README.md +3 -3
- package/docs/connected-host.md +2 -2
- package/docs/getting-started.md +11 -9
- package/docs/tools-and-commands.md +12 -10
- package/docs/voice-and-live-tts.md +2 -2
- package/package.json +1 -1
- package/release/release-notes.md +17 -5
- package/release/release-readiness.json +36 -36
- package/src/agent/competitive-feature-inventory.ts +42 -44
- package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
- package/src/agent/setup-wizard.ts +245 -9
- package/src/input/agent-workspace-categories.ts +2 -2
- package/src/input/agent-workspace-onboarding-categories.ts +4 -4
- package/src/input/agent-workspace-settings.ts +52 -1
- package/src/input/agent-workspace-setup-snapshot.ts +20 -1
- package/src/input/agent-workspace-setup.ts +32 -5
- package/src/input/agent-workspace-snapshot.ts +23 -3
- package/src/input/agent-workspace.ts +5 -1
- package/src/input/setup-wizard-live-receipts.ts +76 -0
- package/src/renderer/agent-workspace-context-lines.ts +21 -6
- package/src/renderer/agent-workspace.ts +46 -10
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
- package/src/tools/agent-harness-agent-orchestration.ts +216 -128
- package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
- package/src/tools/agent-harness-autonomy-queue.ts +19 -8
- package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
- package/src/tools/agent-harness-background-processes.ts +8 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
- package/src/tools/agent-harness-browser-control.ts +12 -4
- package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
- package/src/tools/agent-harness-device-live-read-models.ts +366 -0
- package/src/tools/agent-harness-execution-posture.ts +3 -0
- package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
- package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
- package/src/tools/agent-harness-local-model-smoke.ts +277 -0
- package/src/tools/agent-harness-local-model-url.ts +78 -0
- package/src/tools/agent-harness-media-posture.ts +27 -12
- package/src/tools/agent-harness-memory-external-providers.ts +796 -0
- package/src/tools/agent-harness-memory-posture.ts +253 -137
- package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
- package/src/tools/agent-harness-memory-refinement.ts +340 -0
- package/src/tools/agent-harness-mode-catalog.ts +4 -2
- package/src/tools/agent-harness-model-provider-health.ts +139 -42
- package/src/tools/agent-harness-model-readiness.ts +31 -5
- package/src/tools/agent-harness-model-routing-types.ts +61 -0
- package/src/tools/agent-harness-model-routing.ts +31 -6
- package/src/tools/agent-harness-pairing-posture.ts +30 -9
- package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
- package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
- package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
- package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
- package/src/tools/agent-harness-personal-ops-records.ts +176 -224
- package/src/tools/agent-harness-personal-ops-types.ts +19 -1
- package/src/tools/agent-harness-personal-ops.ts +18 -11
- package/src/tools/agent-harness-remote-read-models.ts +541 -0
- package/src/tools/agent-harness-research-briefing.ts +26 -7
- package/src/tools/agent-harness-research-live-read-models.ts +500 -0
- package/src/tools/agent-harness-research-runs.ts +15 -3
- package/src/tools/agent-harness-research-workflow.ts +92 -19
- package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
- package/src/tools/agent-harness-setup-smoke.ts +26 -1
- package/src/tools/agent-harness-tool-schema.ts +23 -1
- package/src/tools/agent-harness-tool-types.ts +5 -0
- package/src/tools/agent-harness-tool.ts +9 -1
- package/src/tools/agent-harness-workspace-actions.ts +1 -1
- package/src/tools/agent-memory-tool.ts +40 -1
- package/src/tools/agent-model-compare-run.ts +13 -0
- package/src/tools/agent-research-runner.ts +367 -0
- package/src/tools/agent-research-tool.ts +7 -179
- package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
import { previewHarnessText } from './agent-harness-text.ts';
|
|
3
|
+
import { readRecord, readString } from './agent-harness-model-routing-utils.ts';
|
|
4
|
+
import { redactedPersonalOpsText } from './agent-harness-personal-ops-runner.ts';
|
|
5
|
+
import { personalOpsRecordCertification } from './agent-harness-personal-ops-certification.ts';
|
|
6
|
+
import { safeRecordIdPart } from './agent-harness-personal-ops-records.ts';
|
|
7
|
+
import type { PersonalOpsFollowUpRoute, PersonalOpsLaneId, PersonalOpsLiveRecord, PersonalOpsRecordFreshness } from './agent-harness-personal-ops-types.ts';
|
|
8
|
+
|
|
9
|
+
type ProviderBackedLaneId = Extract<PersonalOpsLaneId, 'inbox' | 'calendar'>;
|
|
10
|
+
type ProviderReadModelSourceKind = Extract<PersonalOpsRecordFreshness['source'], 'daemon-read-model' | 'sdk-read-model'>;
|
|
11
|
+
|
|
12
|
+
interface ProviderReadModelSource {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly source: unknown;
|
|
15
|
+
readonly kind: ProviderReadModelSourceKind;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const INBOX_ROUTE_KEYS = {
|
|
19
|
+
read: ['readRoute', 'refreshRoute', 'inspectRoute', 'threadRoute', 'messageRoute', 'getThreadRoute'],
|
|
20
|
+
reply: ['replyRoute', 'sendReplyRoute', 'draftReplyRoute'],
|
|
21
|
+
send: ['sendRoute', 'sendMessageRoute'],
|
|
22
|
+
archive: ['archiveRoute'],
|
|
23
|
+
label: ['labelRoute', 'applyLabelRoute'],
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
const CALENDAR_ROUTE_KEYS = {
|
|
27
|
+
read: ['readRoute', 'refreshRoute', 'inspectRoute', 'eventRoute', 'getEventRoute'],
|
|
28
|
+
edit: ['editRoute', 'updateRoute', 'rescheduleRoute'],
|
|
29
|
+
rsvp: ['rsvpRoute', 'respondRoute'],
|
|
30
|
+
delete: ['deleteRoute', 'cancelRoute'],
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
const direct = readString(record[key]);
|
|
36
|
+
if (direct) return direct;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBoolean(value: unknown): boolean | null {
|
|
42
|
+
if (typeof value === 'boolean') return value;
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
const normalized = value.trim().toLowerCase();
|
|
45
|
+
if (['true', 'yes', '1', 'unread', 'conflict', 'conflicted'].includes(normalized)) return true;
|
|
46
|
+
if (['false', 'no', '0', 'read', 'clear'].includes(normalized)) return false;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readStringArray(value: unknown): readonly string[] {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return [...new Set(value.map((entry) => readString(entry)).filter(Boolean))].slice(0, 12);
|
|
54
|
+
}
|
|
55
|
+
const text = readString(value);
|
|
56
|
+
return text ? text.split(',').map((entry) => entry.trim()).filter(Boolean).slice(0, 12) : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readIso(value: unknown): string {
|
|
60
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
61
|
+
const text = readString(value);
|
|
62
|
+
if (!text) return '';
|
|
63
|
+
const timestamp = Date.parse(text);
|
|
64
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : previewHarnessText(redactedPersonalOpsText(text), 80);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readRoute(value: unknown): string {
|
|
68
|
+
const direct = readString(value);
|
|
69
|
+
if (direct) return direct;
|
|
70
|
+
const record = readRecord(value);
|
|
71
|
+
return firstString(record, ['modelRoute', 'route', 'command', 'action', 'href']);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function routeFromKeys(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const route = readRoute(record[key]);
|
|
77
|
+
if (route) return route;
|
|
78
|
+
}
|
|
79
|
+
const routes = readRecord(record.routes);
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
const route = readRoute(routes[key]);
|
|
82
|
+
if (route) return route;
|
|
83
|
+
}
|
|
84
|
+
const effectRoutes = readRecord(record.effectRoutes);
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const route = readRoute(effectRoutes[key]);
|
|
87
|
+
if (route) return route;
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function providerReadModelSources(context: CommandContext, laneId: ProviderBackedLaneId): readonly ProviderReadModelSource[] {
|
|
93
|
+
const platform = context.platform as unknown as Record<string, unknown>;
|
|
94
|
+
const clients = readRecord(context.clients);
|
|
95
|
+
const readModels = readRecord(platform.readModels);
|
|
96
|
+
const personalOps = readRecord(readModels.personalOps);
|
|
97
|
+
const email = readRecord(readModels.email);
|
|
98
|
+
const mail = readRecord(readModels.mail);
|
|
99
|
+
const calendar = readRecord(readModels.calendar);
|
|
100
|
+
const platformPersonalOps = readRecord(platform.personalOps);
|
|
101
|
+
const operator = readRecord(clients.operator);
|
|
102
|
+
const operatorPersonalOps = readRecord(operator.personalOps);
|
|
103
|
+
if (laneId === 'inbox') {
|
|
104
|
+
return [
|
|
105
|
+
{ path: 'context.platform.readModels.personalOps.inboxThreads', source: personalOps.inboxThreads, kind: 'daemon-read-model' },
|
|
106
|
+
{ path: 'context.platform.readModels.personalOps.emailThreads', source: personalOps.emailThreads, kind: 'daemon-read-model' },
|
|
107
|
+
{ path: 'context.platform.readModels.inboxThreads', source: readModels.inboxThreads, kind: 'daemon-read-model' },
|
|
108
|
+
{ path: 'context.platform.readModels.emailThreads', source: readModels.emailThreads, kind: 'daemon-read-model' },
|
|
109
|
+
{ path: 'context.platform.readModels.email.threads', source: email.threads, kind: 'daemon-read-model' },
|
|
110
|
+
{ path: 'context.platform.readModels.email.messages', source: email.messages, kind: 'daemon-read-model' },
|
|
111
|
+
{ path: 'context.platform.readModels.mail.threads', source: mail.threads, kind: 'daemon-read-model' },
|
|
112
|
+
{ path: 'context.platform.personalOps.inboxThreads', source: platformPersonalOps.inboxThreads, kind: 'daemon-read-model' },
|
|
113
|
+
{ path: 'context.clients.operator.personalOps.inboxThreads', source: operatorPersonalOps.inboxThreads, kind: 'sdk-read-model' },
|
|
114
|
+
{ path: 'context.clients.operator.personalOps.emailThreads', source: operatorPersonalOps.emailThreads, kind: 'sdk-read-model' },
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
return [
|
|
118
|
+
{ path: 'context.platform.readModels.personalOps.calendarEvents', source: personalOps.calendarEvents, kind: 'daemon-read-model' },
|
|
119
|
+
{ path: 'context.platform.readModels.calendarEvents', source: readModels.calendarEvents, kind: 'daemon-read-model' },
|
|
120
|
+
{ path: 'context.platform.readModels.calendar.events', source: calendar.events, kind: 'daemon-read-model' },
|
|
121
|
+
{ path: 'context.platform.readModels.calendar.agenda', source: calendar.agenda, kind: 'daemon-read-model' },
|
|
122
|
+
{ path: 'context.platform.personalOps.calendarEvents', source: platformPersonalOps.calendarEvents, kind: 'daemon-read-model' },
|
|
123
|
+
{ path: 'context.clients.operator.personalOps.calendarEvents', source: operatorPersonalOps.calendarEvents, kind: 'sdk-read-model' },
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readProviderSnapshot(source: unknown, laneId: ProviderBackedLaneId): unknown {
|
|
128
|
+
if (typeof source === 'function') {
|
|
129
|
+
try {
|
|
130
|
+
const result = (source as () => unknown)();
|
|
131
|
+
return result instanceof Promise ? undefined : result;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const record = readRecord(source);
|
|
137
|
+
const methodNames = laneId === 'calendar'
|
|
138
|
+
? ['getSnapshot', 'snapshot', 'listEvents', 'getEvents', 'list']
|
|
139
|
+
: ['getSnapshot', 'snapshot', 'listThreads', 'getThreads', 'listMessages', 'getMessages', 'list'];
|
|
140
|
+
for (const methodName of methodNames) {
|
|
141
|
+
const method = record[methodName];
|
|
142
|
+
if (typeof method !== 'function') continue;
|
|
143
|
+
try {
|
|
144
|
+
const result = (method as () => unknown).call(source);
|
|
145
|
+
return result instanceof Promise ? undefined : result;
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return source;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function recordsFromSnapshot(snapshot: unknown, laneId: ProviderBackedLaneId, depth = 0): readonly unknown[] {
|
|
154
|
+
if (Array.isArray(snapshot)) return snapshot;
|
|
155
|
+
const record = readRecord(snapshot);
|
|
156
|
+
const keys = laneId === 'calendar'
|
|
157
|
+
? ['calendarEvents', 'events', 'agenda', 'items', 'records', 'entries', 'queue']
|
|
158
|
+
: ['inboxThreads', 'emailThreads', 'threads', 'messages', 'emails', 'items', 'records', 'entries', 'queue'];
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
const value = record[key];
|
|
161
|
+
if (Array.isArray(value)) return value;
|
|
162
|
+
const valueRecord = readRecord(value);
|
|
163
|
+
if (Object.keys(valueRecord).length > 0) {
|
|
164
|
+
return Object.entries(valueRecord).map(([providerRecordId, entry]) => {
|
|
165
|
+
const entryRecord = readRecord(entry);
|
|
166
|
+
return Object.keys(entryRecord).length > 0 && !firstString(entryRecord, ['id', 'threadId', 'messageId', 'eventId', 'uid'])
|
|
167
|
+
? { ...entryRecord, providerRecordId }
|
|
168
|
+
: entry;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (depth < 2) {
|
|
173
|
+
for (const key of ['snapshot', 'data', 'current', 'payload', 'result', 'state']) {
|
|
174
|
+
const nested = recordsFromSnapshot(record[key], laneId, depth + 1);
|
|
175
|
+
if (nested.length > 0) return nested;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return firstString(record, laneId === 'calendar'
|
|
179
|
+
? ['id', 'eventId', 'uid', 'providerRecordId']
|
|
180
|
+
: ['id', 'threadId', 'messageId', 'providerRecordId'])
|
|
181
|
+
? [record]
|
|
182
|
+
: [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function providerFreshness(options: {
|
|
186
|
+
readonly source: ProviderReadModelSource;
|
|
187
|
+
readonly readRoute: string;
|
|
188
|
+
readonly requiredFields: readonly string[];
|
|
189
|
+
readonly sampleInput: Readonly<Record<string, unknown>>;
|
|
190
|
+
readonly currentAt: string;
|
|
191
|
+
}): PersonalOpsRecordFreshness {
|
|
192
|
+
return {
|
|
193
|
+
status: options.readRoute ? 'fresh-provider-route-ready' : 'fresh-provider-record-current',
|
|
194
|
+
source: options.source.kind,
|
|
195
|
+
sourceTool: options.source.path,
|
|
196
|
+
...(options.currentAt ? { lastReviewedAt: options.currentAt } : {}),
|
|
197
|
+
...(options.readRoute ? { refreshRoute: options.readRoute } : {}),
|
|
198
|
+
...(options.requiredFields.length > 0 ? { requiredFields: options.requiredFields } : {}),
|
|
199
|
+
...(Object.keys(options.sampleInput).length > 0 ? { sampleInput: options.sampleInput } : {}),
|
|
200
|
+
policy: options.readRoute
|
|
201
|
+
? 'The provider published a current read-model record plus a bounded read or refresh route. Run the route only for a user-requested refresh; provider mutations remain separate confirmed follow-up routes.'
|
|
202
|
+
: 'The provider published current redacted queue state through the daemon or SDK read model. Inspect it as read-only; refreshes and provider mutations require explicit published routes.',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function followUp(
|
|
207
|
+
id: string,
|
|
208
|
+
label: string,
|
|
209
|
+
route: string,
|
|
210
|
+
effect: PersonalOpsFollowUpRoute['effect'],
|
|
211
|
+
policy: string,
|
|
212
|
+
): PersonalOpsFollowUpRoute | null {
|
|
213
|
+
if (!route) return null;
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
label,
|
|
217
|
+
effect,
|
|
218
|
+
modelRoute: route,
|
|
219
|
+
requiresConfirmation: effect === 'confirmed-effect',
|
|
220
|
+
policy,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeInboxRecord(source: ProviderReadModelSource, raw: unknown, index: number): PersonalOpsLiveRecord | null {
|
|
225
|
+
const record = readRecord(raw);
|
|
226
|
+
const providerId = firstString(record, ['providerId', 'provider', 'accountId', 'service', 'mailbox']) || 'provider';
|
|
227
|
+
const threadId = firstString(record, ['threadId', 'conversationId', 'messageThreadId', 'id', 'messageId', 'uid', 'providerRecordId']) || `${index + 1}`;
|
|
228
|
+
const subject = firstString(record, ['subject', 'title', 'summary', 'name']) || `Inbox thread ${index + 1}`;
|
|
229
|
+
const status = firstString(record, ['status', 'state', 'reviewState'])
|
|
230
|
+
|| (readBoolean(record.needsReply) === true ? 'needs-reply' : readBoolean(record.unread) === true ? 'unread' : 'current');
|
|
231
|
+
const sender = firstString(record, ['from', 'sender', 'author', 'participant']);
|
|
232
|
+
const receivedAt = readIso(record.receivedAt ?? record.updatedAt ?? record.timestamp ?? record.date);
|
|
233
|
+
const labels = readStringArray(record.labels ?? record.tags ?? record.mailboxLabels).slice(0, 6);
|
|
234
|
+
const snippet = firstString(record, ['snippet', 'preview', 'excerpt', 'abstract']);
|
|
235
|
+
const readRoute = routeFromKeys(record, INBOX_ROUTE_KEYS.read);
|
|
236
|
+
const requiredFields = readStringArray(record.requiredFields);
|
|
237
|
+
const sampleInput: Record<string, unknown> = threadId ? { threadId } : {};
|
|
238
|
+
const followUpRoutes = [
|
|
239
|
+
followUp('inspect-provider-thread', 'Inspect current thread record', readRoute || `personal_ops action:"lane" laneId:"inbox" query:"${safeRecordIdPart(threadId)}" includeParameters:true`, 'read-only', 'Inspecting the current provider-backed thread record is read-only and does not send, label, archive, move, or delete provider data.'),
|
|
240
|
+
followUp('reply-provider-thread', 'Reply to thread', routeFromKeys(record, INBOX_ROUTE_KEYS.reply), 'confirmed-effect', 'Replying sends or stages provider data only after the user reviews exact recipients and body and confirms the published route.'),
|
|
241
|
+
followUp('send-provider-message', 'Send provider message', routeFromKeys(record, INBOX_ROUTE_KEYS.send), 'confirmed-effect', 'Sending provider mail requires a separate published send route, exact body review, and explicit confirmation.'),
|
|
242
|
+
followUp('archive-provider-thread', 'Archive thread', routeFromKeys(record, INBOX_ROUTE_KEYS.archive), 'confirmed-effect', 'Archiving mutates provider state and requires an explicit user request plus confirmation.'),
|
|
243
|
+
followUp('label-provider-thread', 'Apply label', routeFromKeys(record, INBOX_ROUTE_KEYS.label), 'confirmed-effect', 'Labels mutate provider state and require an explicit user request plus confirmation.'),
|
|
244
|
+
].filter((route): route is PersonalOpsFollowUpRoute => route !== null);
|
|
245
|
+
const hasConfirmedEffectRoute = followUpRoutes.some((route) => route.effect === 'confirmed-effect');
|
|
246
|
+
const currentAt = readIso(record.currentAt ?? record.readAt ?? record.syncedAt ?? record.updatedAt ?? record.receivedAt);
|
|
247
|
+
return {
|
|
248
|
+
id: `provider-thread:${safeRecordIdPart(source.path)}:${safeRecordIdPart(threadId)}`,
|
|
249
|
+
label: `Fresh thread: ${previewHarnessText(redactedPersonalOpsText(subject), 96)}`,
|
|
250
|
+
status: previewHarnessText(redactedPersonalOpsText(status), 48),
|
|
251
|
+
summary: [
|
|
252
|
+
`Provider ${previewHarnessText(redactedPersonalOpsText(providerId), 64)} published current inbox thread ${previewHarnessText(redactedPersonalOpsText(threadId), 80)} from ${source.path}.`,
|
|
253
|
+
sender ? `From ${previewHarnessText(redactedPersonalOpsText(sender), 80)}.` : '',
|
|
254
|
+
labels.length > 0 ? `Labels ${labels.map((label) => previewHarnessText(redactedPersonalOpsText(label), 40)).join(', ')}.` : '',
|
|
255
|
+
receivedAt ? `Updated ${receivedAt}.` : '',
|
|
256
|
+
snippet ? `Preview ${previewHarnessText(redactedPersonalOpsText(snippet), 160)}.` : '',
|
|
257
|
+
followUpRoutes.some((route) => route.effect === 'confirmed-effect') ? 'Published provider effects are separated into confirmed follow-up routes.' : 'No provider mutation route is published for this thread.',
|
|
258
|
+
].filter(Boolean).join(' '),
|
|
259
|
+
userRoute: 'Agent Workspace -> Personal Ops -> Inbox provider queue',
|
|
260
|
+
modelRoute: readRoute || `personal_ops action:"lane" laneId:"inbox" query:"${safeRecordIdPart(threadId)}" includeParameters:true`,
|
|
261
|
+
tags: ['provider-backed', 'fresh-provider', 'inbox-thread', providerId, status, ...labels].filter(Boolean),
|
|
262
|
+
effect: 'read-only',
|
|
263
|
+
capability: 'inbox-provider-thread',
|
|
264
|
+
confirmationRequired: false,
|
|
265
|
+
sourceTool: source.path,
|
|
266
|
+
certification: personalOpsRecordCertification({
|
|
267
|
+
record,
|
|
268
|
+
sourcePath: source.path,
|
|
269
|
+
durableId: threadId,
|
|
270
|
+
recordKind: 'inbox thread',
|
|
271
|
+
hasConfirmedEffectRoute,
|
|
272
|
+
}),
|
|
273
|
+
freshness: providerFreshness({ source, readRoute, requiredFields, sampleInput, currentAt }),
|
|
274
|
+
followUpRoutes,
|
|
275
|
+
...(requiredFields.length > 0 ? { requiredFields } : {}),
|
|
276
|
+
...(Object.keys(sampleInput).length > 0 ? { sampleInput } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeCalendarRecord(source: ProviderReadModelSource, raw: unknown, index: number): PersonalOpsLiveRecord | null {
|
|
281
|
+
const record = readRecord(raw);
|
|
282
|
+
const providerId = firstString(record, ['providerId', 'provider', 'accountId', 'service', 'calendarProvider']) || 'provider';
|
|
283
|
+
const eventId = firstString(record, ['eventId', 'uid', 'id', 'providerRecordId']) || `${index + 1}`;
|
|
284
|
+
const calendarId = firstString(record, ['calendarId', 'calendar', 'calendarName']);
|
|
285
|
+
const title = firstString(record, ['title', 'summary', 'subject', 'name']) || `Calendar event ${index + 1}`;
|
|
286
|
+
const start = readIso(record.start ?? record.startAt ?? record.startTime ?? record.when);
|
|
287
|
+
const end = readIso(record.end ?? record.endAt ?? record.endTime);
|
|
288
|
+
const conflictCount = Array.isArray(record.conflicts) ? record.conflicts.length : readBoolean(record.conflict ?? record.hasConflict) === true ? 1 : 0;
|
|
289
|
+
const status = conflictCount > 0
|
|
290
|
+
? 'conflict'
|
|
291
|
+
: firstString(record, ['status', 'state', 'reviewState']) || 'current';
|
|
292
|
+
const location = firstString(record, ['location', 'room', 'venue']);
|
|
293
|
+
const attendeeCount = Array.isArray(record.attendees) ? record.attendees.length : null;
|
|
294
|
+
const snippet = firstString(record, ['snippet', 'preview', 'notes', 'descriptionSummary']);
|
|
295
|
+
const readRoute = routeFromKeys(record, CALENDAR_ROUTE_KEYS.read);
|
|
296
|
+
const requiredFields = readStringArray(record.requiredFields);
|
|
297
|
+
const sampleInput: Record<string, unknown> = { eventId };
|
|
298
|
+
if (calendarId) sampleInput.calendarId = calendarId;
|
|
299
|
+
const followUpRoutes = [
|
|
300
|
+
followUp('inspect-provider-event', 'Inspect current event record', readRoute || `personal_ops action:"lane" laneId:"calendar" query:"${safeRecordIdPart(eventId)}" includeParameters:true`, 'read-only', 'Inspecting the current provider-backed event record is read-only and does not edit, RSVP, delete, or reschedule provider data.'),
|
|
301
|
+
followUp('edit-provider-event', 'Edit or reschedule event', routeFromKeys(record, CALENDAR_ROUTE_KEYS.edit), 'confirmed-effect', 'Editing or rescheduling a provider event requires exact field review and explicit confirmation.'),
|
|
302
|
+
followUp('rsvp-provider-event', 'RSVP to event', routeFromKeys(record, CALENDAR_ROUTE_KEYS.rsvp), 'confirmed-effect', 'RSVP mutates provider state and requires an explicit user request plus confirmation.'),
|
|
303
|
+
followUp('delete-provider-event', 'Delete or cancel event', routeFromKeys(record, CALENDAR_ROUTE_KEYS.delete), 'confirmed-effect', 'Deleting or cancelling calendar data requires an explicit user request plus confirmation.'),
|
|
304
|
+
].filter((route): route is PersonalOpsFollowUpRoute => route !== null);
|
|
305
|
+
const hasConfirmedEffectRoute = followUpRoutes.some((route) => route.effect === 'confirmed-effect');
|
|
306
|
+
const currentAt = readIso(record.currentAt ?? record.readAt ?? record.syncedAt ?? record.updatedAt ?? record.start);
|
|
307
|
+
return {
|
|
308
|
+
id: `provider-event:${safeRecordIdPart(source.path)}:${safeRecordIdPart(eventId)}`,
|
|
309
|
+
label: `Fresh event: ${previewHarnessText(redactedPersonalOpsText(title), 96)}`,
|
|
310
|
+
status: previewHarnessText(redactedPersonalOpsText(status), 48),
|
|
311
|
+
summary: [
|
|
312
|
+
`Provider ${previewHarnessText(redactedPersonalOpsText(providerId), 64)} published current calendar event ${previewHarnessText(redactedPersonalOpsText(eventId), 80)} from ${source.path}.`,
|
|
313
|
+
calendarId ? `Calendar ${previewHarnessText(redactedPersonalOpsText(calendarId), 64)}.` : '',
|
|
314
|
+
start || end ? `Window ${[start, end].filter(Boolean).join(' -> ')}.` : '',
|
|
315
|
+
location ? `Location ${previewHarnessText(redactedPersonalOpsText(location), 80)}.` : '',
|
|
316
|
+
attendeeCount !== null ? `${attendeeCount} attendee(s).` : '',
|
|
317
|
+
conflictCount > 0 ? `${conflictCount} conflict signal(s).` : '',
|
|
318
|
+
snippet ? `Preview ${previewHarnessText(redactedPersonalOpsText(snippet), 160)}.` : '',
|
|
319
|
+
followUpRoutes.some((route) => route.effect === 'confirmed-effect') ? 'Published provider effects are separated into confirmed follow-up routes.' : 'No provider mutation route is published for this event.',
|
|
320
|
+
].filter(Boolean).join(' '),
|
|
321
|
+
userRoute: 'Agent Workspace -> Personal Ops -> Calendar provider queue',
|
|
322
|
+
modelRoute: readRoute || `personal_ops action:"lane" laneId:"calendar" query:"${safeRecordIdPart(eventId)}" includeParameters:true`,
|
|
323
|
+
tags: ['provider-backed', 'fresh-provider', 'calendar-event', providerId, status, conflictCount > 0 ? 'conflict' : '', calendarId].filter(Boolean),
|
|
324
|
+
effect: 'read-only',
|
|
325
|
+
capability: 'calendar-provider-event',
|
|
326
|
+
confirmationRequired: false,
|
|
327
|
+
sourceTool: source.path,
|
|
328
|
+
certification: personalOpsRecordCertification({
|
|
329
|
+
record,
|
|
330
|
+
sourcePath: source.path,
|
|
331
|
+
durableId: eventId,
|
|
332
|
+
recordKind: 'calendar event',
|
|
333
|
+
hasConfirmedEffectRoute,
|
|
334
|
+
}),
|
|
335
|
+
freshness: providerFreshness({ source, readRoute, requiredFields, sampleInput, currentAt }),
|
|
336
|
+
followUpRoutes,
|
|
337
|
+
...(requiredFields.length > 0 ? { requiredFields } : {}),
|
|
338
|
+
...(Object.keys(sampleInput).length > 0 ? { sampleInput } : {}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function providerBackedQueueRecords(context: CommandContext, laneId: ProviderBackedLaneId): readonly PersonalOpsLiveRecord[] {
|
|
343
|
+
const records: PersonalOpsLiveRecord[] = [];
|
|
344
|
+
const seen = new Set<string>();
|
|
345
|
+
for (const source of providerReadModelSources(context, laneId)) {
|
|
346
|
+
const snapshot = readProviderSnapshot(source.source, laneId);
|
|
347
|
+
for (const [index, raw] of recordsFromSnapshot(snapshot, laneId).entries()) {
|
|
348
|
+
const record = laneId === 'calendar'
|
|
349
|
+
? normalizeCalendarRecord(source, raw, index)
|
|
350
|
+
: normalizeInboxRecord(source, raw, index);
|
|
351
|
+
if (!record || seen.has(record.id)) continue;
|
|
352
|
+
seen.add(record.id);
|
|
353
|
+
records.push(record);
|
|
354
|
+
if (records.length >= 10) return records;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return records;
|
|
358
|
+
}
|