@mastra/factory 0.24.0-alpha.9 → 0.24.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 +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +111 -19
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +1 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +24 -13
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +53 -9
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +87 -34
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +0 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +1 -4
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +1 -1
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/config.d.ts +6 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +20 -19
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +33 -31
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +6 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +19 -65
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +6 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +95 -12
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +11 -9
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +10 -10
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/channel-reply-processor.d.ts +7 -6
- package/dist/session/channel-reply-processor.d.ts.map +1 -1
- package/dist/session/channel-reply-processor.js +9 -9
- package/dist/session/channel-reply-processor.js.map +1 -1
- package/dist/session/channel-session-link-processor.js +1 -0
- package/dist/session/channel-session-link-processor.js.map +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +7 -6
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +105 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +195 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/session-threads.d.ts +58 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +90 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.js +8 -8
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +3 -4
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/skills/service.d.ts +6 -8
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +3 -3
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +19 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +42 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +25 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/approval-tools.js +1 -1
- package/dist/tools/approval-tools.js.map +1 -1
- package/dist/tools/deliver.d.ts +8 -5
- package/dist/tools/deliver.d.ts.map +1 -1
- package/dist/tools/deliver.js +22 -8
- package/dist/tools/deliver.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +203 -72
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/permissions.d.ts +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +1 -1
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +10 -3
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +3 -2
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +33 -16
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/side-sessions.d.ts +1 -1
- package/dist/tools/side-sessions.d.ts.map +1 -1
- package/dist/tools/side-sessions.js +1 -1
- package/dist/tools/side-sessions.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +28 -15
- package/dist/workspace.js.map +1 -1
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reports.js","names":[],"sources":["../../src/tools/reports.ts"],"sourcesContent":["/**\n * Reports from sessions doing the work back to whoever follows it: a side\n * session's origin, or every subscriber of a work item. The agent reports with\n * `factory_message_origin`; when a run ends without one, the run-end backstop\n * sends it, so each run produces exactly one final report per recipient.\n */\n\nimport type {\n NotificationPriority,\n NotificationsStorage,\n SendNotificationSignalInput,\n} from '@mastra/core/notifications';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_SESSION_NOTIFICATION_SOURCE } from './deliver.js';\nimport type { FactoryDeliveryResult, FactoryDeliveryTarget } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\n\nexport type FactoryReportStatus = 'progress' | 'blocked' | 'done' | 'failed';\n\n/** How each report status travels. Final reports never coalesce, so a later report can't overwrite them. */\nexport const FACTORY_REPORT_FORMS: Record<\n FactoryReportStatus | 'report',\n { kind: string; priority: NotificationPriority; coalesce: boolean }\n> = {\n failed: { kind: 'failed', priority: 'urgent', coalesce: false },\n blocked: { kind: 'blocked', priority: 'urgent', coalesce: false },\n done: { kind: 'done', priority: 'high', coalesce: false },\n progress: { kind: 'progress', priority: 'low', coalesce: true },\n // Not `message`: only messages can be answered, so a report can't start a reply loop.\n report: { kind: 'report', priority: 'high', coalesce: false },\n};\n\n/** How many recent `factory-session` notifications on a recipient thread the final-report check reads. */\nconst FINAL_REPORT_SCAN = 100;\n\nexport interface FactoryReportSender {\n sessionId: string;\n threadId: string;\n userId: string;\n}\n\nexport type FactoryReportDeliver = (\n target: FactoryDeliveryTarget,\n notification: Omit<SendNotificationSignalInput, 'source'>,\n options?: { onlyIfLive?: boolean },\n) => Promise<FactoryDeliveryResult>;\n\nexport interface FactoryReporting {\n deliver: FactoryReportDeliver;\n notifications: Pick<NotificationsStorage, 'listNotifications'>;\n}\n\n/**\n * Whether `senderSessionId` already put a report of one of `kinds` on the\n * recipient thread since `since`. Reads the durable notifications store, so it\n * holds across restarts. `listNotifications` has no time filter: records come\n * newest-updated first, and the scan stops at the first one last updated\n * before `since` (nothing after it can have been created later).\n */\nexport async function hasFinalReport(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n since,\n kinds,\n agentOnly = false,\n }: {\n senderSessionId: string;\n recipientThreadId: string;\n since: Date;\n kinds: readonly string[];\n /** Ignore reports Factory sent on the session's behalf (the backstop). */\n agentOnly?: boolean;\n },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n for (const record of records) {\n if (record.updatedAt < since) break;\n if (\n record.attributes?.['from-session'] === senderSessionId &&\n !(agentOnly && record.attributes?.backstop === 'true') &&\n // A reply carrying a final status counts like the report itself.\n (kinds.includes(record.kind) ||\n (record.kind === 'reply' &&\n typeof record.attributes?.status === 'string' &&\n kinds.includes(record.attributes.status))) &&\n record.createdAt >= since\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether the newest report `senderSessionId` put on the recipient thread is\n * already this exact `failed` backstop. A session stuck failing the same way\n * (a run that dies before the model, restarted over and over) then reports\n * once, not once per attempt; any other report in between resets it.\n */\nexport async function repeatsLastFailure(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n summary,\n }: { senderSessionId: string; recipientThreadId: string; summary: string },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n // Storage orders by `updatedAt`, which moves when a report is read; the\n // newest report is the one created last.\n const last = records\n .filter(record => record.attributes?.['from-session'] === senderSessionId)\n .reduce<(typeof records)[number] | undefined>(\n (newest, record) => (!newest || record.createdAt > newest.createdAt ? record : newest),\n undefined,\n );\n return last?.kind === 'failed' && last.attributes?.backstop === 'true' && last.summary === summary;\n}\n\nexport function reportNotification({\n sender,\n status,\n message,\n attributes = {},\n}: {\n sender: FactoryReportSender;\n status?: FactoryReportStatus;\n message: string;\n attributes?: Record<string, string>;\n}): Omit<SendNotificationSignalInput, 'source'> {\n const form = FACTORY_REPORT_FORMS[status ?? 'report'];\n return {\n kind: form.kind,\n priority: form.priority,\n // The whole text: a notification's summary is all the recipient's agent ever sees, even\n // through notification_inbox (which hides payloads), so a cut-off report loses its verdict.\n summary: message,\n payload: { message },\n sourceId: sender.sessionId,\n ...(form.coalesce ? { coalesceKey: `factory-session:${sender.sessionId}:${form.kind}` } : {}),\n attributes: {\n ...attributes,\n ...(status ? { status } : {}),\n // Last, so nothing above can claim another sender.\n 'from-session': sender.sessionId,\n 'from-thread': sender.threadId,\n 'from-user': sender.userId,\n },\n };\n}\n\n/** Everyone following a work item, except the session doing the reporting. */\nexport async function workItemReportRecipients(\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers'>,\n { orgId, workItemId, senderSessionId }: { orgId: string; workItemId: string; senderSessionId: string },\n): Promise<FactoryDeliveryTarget[]> {\n const subscribers = await workItems.listWorkItemSubscribers({ orgId, workItemId });\n return subscribers\n .filter(subscriber => subscriber.sessionId !== senderSessionId)\n .map(subscriber => ({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }));\n}\n\n/** How reports name an item: `#n`, else its quoted title, else its id. */\nasync function workItemReference(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n { orgId, factoryProjectId, workItemId }: { orgId: string; factoryProjectId: string; workItemId: string },\n): Promise<{ label: string; ref: string }> {\n const item = await workItems.getForProject(orgId, factoryProjectId, workItemId);\n const number = item ? workItemNumber(item) : null;\n if (number !== null) return { label: `#${number}`, ref: `#${number}` };\n return { label: item ? `\"${item.title}\"` : workItemId, ref: workItemId };\n}\n\n/** Attributes that say which item and role a worker's report is about. */\nexport async function workItemReportAttributes(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n {\n orgId,\n factoryProjectId,\n workItemId,\n role,\n }: { orgId: string; factoryProjectId: string; workItemId: string; role: string },\n): Promise<{ label: string; attributes: Record<string, string> }> {\n const { label, ref } = await workItemReference(workItems, { orgId, factoryProjectId, workItemId });\n return { label, attributes: { 'work-item': ref, role } };\n}\n\nexport type FactoryRunOutcome =\n | { status: 'done' }\n | { status: 'failed'; reason: string; failureCode?: string }\n | { status: 'blocked'; toolName?: string; approval?: boolean };\n\n/** `waiting`: the session is waiting for an answer, so a later run owes the final report. */\nexport interface FactoryRunEndResult {\n waiting: boolean;\n deliveries?: FactoryDeliveryResult[];\n}\n\n/**\n * The run-end backstop: send `outcome` to each recipient that hasn't had a\n * final report from this sender since the run started. An agent's own `done`\n * or `failed` report wins. A run that completes after the agent's own\n * `blocked` sends nothing: the agent ended its turn to wait for an answer, and\n * its `done` comes from a later run. A `failed` backstop still goes out after an\n * earlier `blocked`.\n */\nexport async function sendRunEndReport(\n reporting: FactoryReporting,\n {\n sender,\n recipients,\n since,\n outcome,\n subject,\n attributes = {},\n }: {\n sender: FactoryReportSender;\n recipients: FactoryDeliveryTarget[];\n since: Date;\n outcome: FactoryRunOutcome;\n /** Who finished, e.g. \"The work session for #12\". */\n subject: string;\n attributes?: Record<string, string>;\n },\n): Promise<FactoryRunEndResult> {\n const kinds = outcome.status === 'blocked' ? ['blocked'] : ['done', 'failed'];\n const message =\n outcome.status === 'done'\n ? `${subject} finished.`\n : outcome.status === 'failed'\n ? `${subject} failed${outcome.failureCode ? ` (${outcome.failureCode})` : ''}: ${outcome.reason}`\n : outcome.approval\n ? `${subject} is waiting for someone to approve ${outcome.toolName ?? 'a tool call'} in that session.`\n : `${subject} is waiting${outcome.toolName ? ` on ${outcome.toolName}` : ''} and needs an answer.`;\n const notification = reportNotification({\n sender,\n status: outcome.status,\n message,\n attributes: {\n ...attributes,\n backstop: 'true',\n ...(outcome.status === 'failed' && outcome.failureCode ? { 'failure-code': outcome.failureCode } : {}),\n ...(outcome.status === 'blocked' && outcome.toolName ? { 'parked-tool': outcome.toolName } : {}),\n ...(outcome.status === 'blocked' && outcome.approval ? { 'waiting-for': 'approval' } : {}),\n },\n });\n const report = (recipient: FactoryDeliveryTarget, reportKinds: string[], agentOnly = false) =>\n hasFinalReport(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n since,\n kinds: reportKinds,\n agentOnly,\n });\n let waiting = outcome.status === 'blocked';\n const deliveries: FactoryDeliveryResult[] = [];\n for (const recipient of recipients) {\n // Only the agent's own reports count: an earlier run's backstop lands after this\n // run's window opened. A reply already told that follower how a finished run went.\n if (await report(recipient, outcome.status === 'done' ? [...kinds, 'reply'] : kinds, true)) continue;\n // Only the agent's own `blocked` means it ended its turn to wait; a backstop `blocked` from a parked run doesn't.\n if (outcome.status === 'done' && (await report(recipient, ['blocked'], true))) {\n waiting = true;\n continue;\n }\n if (\n outcome.status === 'failed' &&\n (await repeatsLastFailure(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n summary: message,\n }))\n ) {\n continue;\n }\n deliveries.push(await reporting.deliver(recipient, notification));\n }\n return { waiting, deliveries };\n}\n\nexport interface FactoryRunEndInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n role: string;\n sender: FactoryReportSender;\n since: Date;\n outcome: FactoryRunOutcome;\n}\n\nexport type FactoryRunEndReporter = (input: FactoryRunEndInput) => Promise<FactoryRunEndResult>;\n\n/** The backstop for a work-item run: reports to the item's subscribers. Failures are logged, never thrown. */\nexport function createWorkItemRunEndReporter({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n /** Resolved per report: the controller exists only once the mount is up. */\n reporting: () => Promise<FactoryReporting | null>;\n}): FactoryRunEndReporter {\n return async input => {\n try {\n const resolved = await reporting();\n if (!resolved) return { waiting: false };\n const recipients = await workItemReportRecipients(workItems, {\n orgId: input.orgId,\n workItemId: input.workItemId,\n senderSessionId: input.sender.sessionId,\n });\n if (recipients.length === 0) return { waiting: false };\n const { label, attributes } = await workItemReportAttributes(workItems, input);\n return await sendRunEndReport(resolved, {\n sender: input.sender,\n recipients,\n since: input.since,\n outcome: input.outcome,\n subject: `The ${input.role} session for ${label}`,\n attributes,\n });\n } catch (error) {\n console.warn('[Factory reports] Unable to send the run-end report.', error);\n return { waiting: false };\n }\n };\n}\n\nexport interface FactoryStageChange {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n}\n\n/**\n * Tells a work item's subscribers it moved, at `low` priority. Only sessions\n * already live hear it: a stage change never brings a session online; an\n * offline subscriber sees the stage in its state signal on its next turn.\n */\nexport function createStageChangeNotifier({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n reporting: () => Promise<FactoryReporting | null>;\n}): (change: FactoryStageChange) => Promise<void> {\n return async change => {\n const subscribers = await workItems.listWorkItemSubscribers({\n orgId: change.orgId,\n workItemId: change.workItemId,\n });\n if (subscribers.length === 0) return;\n const resolved = await reporting();\n if (!resolved) return;\n const { label, ref } = await workItemReference(workItems, change);\n const summary = change.fromStage\n ? `${label} moved from ${change.fromStage} to ${change.toStage}.`\n : `${label} moved to ${change.toStage}.`;\n const notification: Omit<SendNotificationSignalInput, 'source'> = {\n kind: 'stage',\n priority: 'low',\n summary,\n payload: { message: summary },\n sourceId: change.workItemId,\n coalesceKey: `factory-stage:${change.workItemId}`,\n attributes: {\n 'work-item': ref,\n ...(change.fromStage ? { 'from-stage': change.fromStage } : {}),\n 'to-stage': change.toStage,\n },\n };\n for (const subscriber of subscribers) {\n try {\n await resolved.deliver({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }, notification, {\n onlyIfLive: true,\n });\n } catch (error) {\n console.warn(`[Factory reports] Unable to tell ${subscriber.sessionId} that ${label} moved.`, error);\n }\n }\n };\n}\n\n/** Reports one run end of a session; the thread is the one the run used. */\nexport type FactorySessionRunEndReporter = (input: {\n sessionId: string;\n threadId: string | null;\n outcome: FactoryRunOutcome;\n since: Date;\n}) => Promise<void>;\n\n/** A tool call a session's run is holding until someone approves or declines it. */\nexport interface FactoryApprovalRequest {\n sessionId: string;\n threadId: string | null;\n toolName: string;\n toolCallId: string;\n args: unknown;\n}\n\nexport type FactoryApprovalNotifier = (request: FactoryApprovalRequest) => Promise<void>;\n\ninterface SessionReportDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<\n WorkItemsStorage,\n 'findActiveRunBindingForSession' | 'findRunBindingBySession' | 'listWorkItemSubscribers' | 'getForProject'\n >;\n reporting: () => Promise<FactoryReporting | null>;\n}\n\n/** How much of a gated call's arguments an approval notice quotes. */\nconst APPROVAL_ARGS_LENGTH = 3000;\n\n/**\n * Who hears from a session: a work session (one with an active run binding)\n * reports to its item's followers, a side session to the session that started\n * it. Any other session (a person's own) has nobody to tell.\n */\nasync function sessionAudience(\n { sessions, workItems }: Omit<SessionReportDependencies, 'reporting'>,\n sessionId: string,\n threadId: string | null,\n /** When the run started: a binding revoked since then (the run moved its card to a finished stage) still counts. */\n since?: Date,\n): Promise<\n | {\n kind: 'work-item';\n binding: NonNullable<Awaited<ReturnType<WorkItemsStorage['findActiveRunBindingForSession']>>>;\n sender: FactoryReportSender;\n }\n | {\n kind: 'origin';\n sender: FactoryReportSender;\n recipient: FactoryDeliveryTarget;\n subject: string;\n /** Set while the origin has muted this side session's reports (approval notices still go through). */\n mutedAt: Date | null;\n }\n | null\n> {\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n let binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId: row.sessionId,\n });\n if (!binding && since && threadId) {\n const latest = await workItems.findRunBindingBySession({\n factoryProjectId: row.factoryProjectId,\n threadId,\n resourceId: row.sessionId,\n sessionId: row.sessionId,\n });\n if (latest?.orgId === row.orgId && latest.revokedAt && latest.revokedAt >= since) binding = latest;\n }\n if (binding) {\n return {\n kind: 'work-item',\n binding,\n sender: { sessionId: row.sessionId, threadId: binding.threadId, userId: row.userId },\n };\n }\n if (!row.originSessionId || !row.originThreadId || !threadId) return null;\n return {\n kind: 'origin',\n sender: { sessionId: row.sessionId, threadId, userId: row.userId },\n recipient: { sessionId: row.originSessionId, threadId: row.originThreadId },\n subject: `The side session \"${row.title ?? row.sessionId}\"`,\n mutedAt: row.originMutedAt ?? null,\n };\n}\n\n/** Reports each run end of a work or side session to whoever follows it. */\nexport function createSessionRunEndReporter(deps: SessionReportDependencies): FactorySessionRunEndReporter {\n const workItemRunEnd = createWorkItemRunEndReporter(deps);\n return async ({ sessionId, threadId, outcome, since }) => {\n const audience = await sessionAudience(deps, sessionId, threadId, since);\n if (!audience) return;\n if (audience.kind === 'work-item') {\n const { binding, sender } = audience;\n await workItemRunEnd({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n sender,\n since,\n outcome,\n });\n return;\n }\n // A muted origin still hears when the session is blocked: nothing else would tell it the session is waiting.\n if (audience.mutedAt && outcome.status !== 'blocked') return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n await sendRunEndReport(resolved, {\n sender: audience.sender,\n recipients: [audience.recipient],\n since,\n outcome,\n subject: audience.subject,\n });\n };\n}\n\n/**\n * Tells whoever follows a session, urgently, the moment its run holds a tool\n * call for approval. The run stays open on the gate, so no run end would say\n * so; the notice names the session and call so a follower can answer it with\n * `factory_answer_approval`.\n */\nexport function createSessionApprovalNotifier(deps: SessionReportDependencies): FactoryApprovalNotifier {\n return async ({ sessionId, threadId, toolName, toolCallId, args }) => {\n const audience = await sessionAudience(deps, sessionId, threadId);\n if (!audience) return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n let subject: string;\n let recipients: FactoryDeliveryTarget[];\n let attributes: Record<string, string> = {};\n if (audience.kind === 'work-item') {\n const { binding } = audience;\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: binding.orgId,\n workItemId: binding.workItemId,\n senderSessionId: audience.sender.sessionId,\n });\n const named = await workItemReportAttributes(deps.workItems, binding);\n subject = `The ${binding.role} session for ${named.label}`;\n attributes = named.attributes;\n } else {\n recipients = [audience.recipient];\n subject = audience.subject;\n }\n if (recipients.length === 0) return;\n const argsText = JSON.stringify(args ?? {}) ?? '{}';\n const quoted = argsText.length > APPROVAL_ARGS_LENGTH ? `${argsText.slice(0, APPROVAL_ARGS_LENGTH)}…` : argsText;\n const message = [\n `${subject} is waiting for approval to run ${toolName} with ${quoted}.`,\n `Answer it with factory_answer_approval (sessionId \"${audience.sender.sessionId}\", toolCallId \"${toolCallId}\"), or open that session to approve or decline it there.`,\n ].join(' ');\n const notification = {\n ...reportNotification({\n sender: audience.sender,\n message,\n attributes: { ...attributes, 'waiting-for': 'approval', 'tool-name': toolName, 'tool-call-id': toolCallId },\n }),\n kind: 'approval',\n priority: 'urgent' as const,\n // One notice per held call, even if the gate is announced again.\n dedupeKey: `factory-approval:${toolCallId}`,\n };\n for (const recipient of recipients) await resolved.deliver(recipient, notification);\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reports.js","names":[],"sources":["../../src/tools/reports.ts"],"sourcesContent":["/**\n * Reports from sessions doing the work back to whoever follows it: a side\n * session's origin, or every subscriber of a work item. The agent reports with\n * `factory_message_origin`; when a run ends without one, the run-end backstop\n * sends it, so each run produces exactly one final report per recipient.\n */\n\nimport type {\n NotificationPriority,\n NotificationsStorage,\n SendNotificationSignalInput,\n} from '@mastra/core/notifications';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_SESSION_NOTIFICATION_SOURCE } from './deliver.js';\nimport type { FactoryDeliveryResult, FactoryDeliveryTarget } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\n\nexport type FactoryReportStatus = 'progress' | 'blocked' | 'done' | 'failed';\n\n/** How each report status travels. Final reports never coalesce, so a later report can't overwrite them. */\nexport const FACTORY_REPORT_FORMS: Record<\n FactoryReportStatus | 'report',\n { kind: string; priority: NotificationPriority; coalesce: boolean }\n> = {\n failed: { kind: 'failed', priority: 'urgent', coalesce: false },\n blocked: { kind: 'blocked', priority: 'urgent', coalesce: false },\n done: { kind: 'done', priority: 'high', coalesce: false },\n progress: { kind: 'progress', priority: 'low', coalesce: true },\n // Not `message`: only messages can be answered, so a report can't start a reply loop.\n report: { kind: 'report', priority: 'high', coalesce: false },\n};\n\n/** How many recent `factory-session` notifications on a recipient thread the final-report check reads. */\nconst FINAL_REPORT_SCAN = 100;\n\nexport interface FactoryReportSender {\n sessionId: string;\n threadId: string;\n userId: string;\n}\n\nexport type FactoryReportDeliver = (\n target: FactoryDeliveryTarget,\n notification: Omit<SendNotificationSignalInput, 'source'>,\n options?: { onlyIfLive?: boolean },\n) => Promise<FactoryDeliveryResult>;\n\nexport interface FactoryReporting {\n deliver: FactoryReportDeliver;\n notifications: Pick<NotificationsStorage, 'listNotifications'>;\n}\n\n/**\n * Whether `senderSessionId` already put a report of one of `kinds` on the\n * recipient thread since `since`. Reads the durable notifications store, so it\n * holds across restarts. `listNotifications` has no time filter: records come\n * newest-updated first, and the scan stops at the first one last updated\n * before `since` (nothing after it can have been created later).\n */\nexport async function hasFinalReport(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n since,\n kinds,\n agentOnly = false,\n }: {\n senderSessionId: string;\n recipientThreadId: string;\n since: Date;\n kinds: readonly string[];\n /** Ignore reports Factory sent on the session's behalf (the backstop). */\n agentOnly?: boolean;\n },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n for (const record of records) {\n if (record.updatedAt < since) break;\n if (\n record.attributes?.['from-session'] === senderSessionId &&\n !(agentOnly && record.attributes?.backstop === 'true') &&\n // A reply carrying a final status counts like the report itself.\n (kinds.includes(record.kind) ||\n (record.kind === 'reply' &&\n typeof record.attributes?.status === 'string' &&\n kinds.includes(record.attributes.status))) &&\n record.createdAt >= since\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether the newest report `senderSessionId` put on the recipient thread is\n * already this exact `failed` backstop. A session stuck failing the same way\n * (a run that dies before the model, restarted over and over) then reports\n * once, not once per attempt; any other report in between resets it.\n */\nexport async function repeatsLastFailure(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n summary,\n }: { senderSessionId: string; recipientThreadId: string; summary: string },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n // Storage orders by `updatedAt`, which moves when a report is read; the\n // newest report is the one created last.\n const last = records\n .filter(record => record.attributes?.['from-session'] === senderSessionId)\n .reduce<(typeof records)[number] | undefined>(\n (newest, record) => (!newest || record.createdAt > newest.createdAt ? record : newest),\n undefined,\n );\n return last?.kind === 'failed' && last.attributes?.backstop === 'true' && last.summary === summary;\n}\n\nexport function reportNotification({\n sender,\n status,\n message,\n attributes = {},\n}: {\n sender: FactoryReportSender;\n status?: FactoryReportStatus;\n message: string;\n attributes?: Record<string, string>;\n}): Omit<SendNotificationSignalInput, 'source'> {\n const form = FACTORY_REPORT_FORMS[status ?? 'report'];\n return {\n kind: form.kind,\n priority: form.priority,\n // The whole text: a notification's summary is all the recipient's agent ever sees, even\n // through notification_inbox (which hides payloads), so a cut-off report loses its verdict.\n summary: message,\n payload: { message },\n sourceId: sender.sessionId,\n ...(form.coalesce ? { coalesceKey: `factory-session:${sender.sessionId}:${form.kind}` } : {}),\n attributes: {\n ...attributes,\n ...(status ? { status } : {}),\n // Last, so nothing above can claim another sender.\n 'from-session': sender.sessionId,\n 'from-thread': sender.threadId,\n 'from-user': sender.userId,\n },\n };\n}\n\n/** Everyone following a work item, except the session doing the reporting. */\nexport async function workItemReportRecipients(\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers'>,\n { orgId, workItemId, senderSessionId }: { orgId: string; workItemId: string; senderSessionId: string },\n): Promise<FactoryDeliveryTarget[]> {\n const subscribers = await workItems.listWorkItemSubscribers({ orgId, workItemId });\n return subscribers\n .filter(subscriber => subscriber.sessionId !== senderSessionId)\n .map(subscriber => ({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }));\n}\n\n/** How reports name an item: `#n`, else its quoted title, else its id. */\nasync function workItemReference(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n { orgId, factoryProjectId, workItemId }: { orgId: string; factoryProjectId: string; workItemId: string },\n): Promise<{ label: string; ref: string }> {\n const item = await workItems.getForProject(orgId, factoryProjectId, workItemId);\n const number = item ? workItemNumber(item) : null;\n if (number !== null) return { label: `#${number}`, ref: `#${number}` };\n return { label: item ? `\"${item.title}\"` : workItemId, ref: workItemId };\n}\n\n/** Attributes that say which item and role a worker's report is about. */\nexport async function workItemReportAttributes(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n {\n orgId,\n factoryProjectId,\n workItemId,\n role,\n }: { orgId: string; factoryProjectId: string; workItemId: string; role: string },\n): Promise<{ label: string; attributes: Record<string, string> }> {\n const { label, ref } = await workItemReference(workItems, { orgId, factoryProjectId, workItemId });\n return { label, attributes: { 'work-item': ref, role } };\n}\n\nexport type FactoryRunOutcome =\n | { status: 'done' }\n | { status: 'failed'; reason: string; failureCode?: string }\n | { status: 'blocked'; toolName?: string; approval?: boolean };\n\n/** `waiting`: the session is waiting for an answer, so a later run owes the final report. */\nexport interface FactoryRunEndResult {\n waiting: boolean;\n deliveries?: FactoryDeliveryResult[];\n}\n\n/**\n * The run-end backstop: send `outcome` to each recipient that hasn't had a\n * final report from this sender since the run started. An agent's own `done`\n * or `failed` report wins. A run that completes after the agent's own\n * `blocked` sends nothing: the agent ended its turn to wait for an answer, and\n * its `done` comes from a later run. A `failed` backstop still goes out after an\n * earlier `blocked`.\n */\nexport async function sendRunEndReport(\n reporting: FactoryReporting,\n {\n sender,\n recipients,\n since,\n outcome,\n subject,\n attributes = {},\n }: {\n sender: FactoryReportSender;\n recipients: FactoryDeliveryTarget[];\n since: Date;\n outcome: FactoryRunOutcome;\n /** Who finished, e.g. \"The work session for #12\". */\n subject: string;\n attributes?: Record<string, string>;\n },\n): Promise<FactoryRunEndResult> {\n const kinds = outcome.status === 'blocked' ? ['blocked'] : ['done', 'failed'];\n const message =\n outcome.status === 'done'\n ? `${subject} finished.`\n : outcome.status === 'failed'\n ? `${subject} failed${outcome.failureCode ? ` (${outcome.failureCode})` : ''}: ${outcome.reason}`\n : outcome.approval\n ? `${subject} is waiting for someone to approve ${outcome.toolName ?? 'a tool call'} in that session.`\n : `${subject} is waiting${outcome.toolName ? ` on ${outcome.toolName}` : ''} and needs an answer.`;\n const notification = reportNotification({\n sender,\n status: outcome.status,\n message,\n attributes: {\n ...attributes,\n backstop: 'true',\n ...(outcome.status === 'failed' && outcome.failureCode ? { 'failure-code': outcome.failureCode } : {}),\n ...(outcome.status === 'blocked' && outcome.toolName ? { 'parked-tool': outcome.toolName } : {}),\n ...(outcome.status === 'blocked' && outcome.approval ? { 'waiting-for': 'approval' } : {}),\n },\n });\n const report = (recipient: FactoryDeliveryTarget, reportKinds: string[], agentOnly = false) =>\n hasFinalReport(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n since,\n kinds: reportKinds,\n agentOnly,\n });\n let waiting = outcome.status === 'blocked';\n const deliveries: FactoryDeliveryResult[] = [];\n for (const recipient of recipients) {\n // Only the agent's own reports count: an earlier run's backstop lands after this\n // run's window opened. A reply already told that follower how a finished run went.\n if (await report(recipient, outcome.status === 'done' ? [...kinds, 'reply'] : kinds, true)) continue;\n // Only the agent's own `blocked` means it ended its turn to wait; a backstop `blocked` from a parked run doesn't.\n if (outcome.status === 'done' && (await report(recipient, ['blocked'], true))) {\n waiting = true;\n continue;\n }\n if (\n outcome.status === 'failed' &&\n (await repeatsLastFailure(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n summary: message,\n }))\n ) {\n continue;\n }\n deliveries.push(await reporting.deliver(recipient, notification));\n }\n return { waiting, deliveries };\n}\n\nexport interface FactoryRunEndInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n role: string;\n sender: FactoryReportSender;\n since: Date;\n outcome: FactoryRunOutcome;\n}\n\nexport type FactoryRunEndReporter = (input: FactoryRunEndInput) => Promise<FactoryRunEndResult>;\n\n/** The backstop for a work-item run: reports to the item's subscribers. Failures are logged, never thrown. */\nexport function createWorkItemRunEndReporter({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n /** Resolved per report: the controller exists only once the mount is up. */\n reporting: () => Promise<FactoryReporting | null>;\n}): FactoryRunEndReporter {\n return async input => {\n try {\n const resolved = await reporting();\n if (!resolved) return { waiting: false };\n const recipients = await workItemReportRecipients(workItems, {\n orgId: input.orgId,\n workItemId: input.workItemId,\n senderSessionId: input.sender.sessionId,\n });\n if (recipients.length === 0) return { waiting: false };\n const { label, attributes } = await workItemReportAttributes(workItems, input);\n return await sendRunEndReport(resolved, {\n sender: input.sender,\n recipients,\n since: input.since,\n outcome: input.outcome,\n subject: `The ${input.role} session for ${label}`,\n attributes,\n });\n } catch (error) {\n console.warn('[Factory reports] Unable to send the run-end report.', error);\n return { waiting: false };\n }\n };\n}\n\nexport interface FactoryStageChange {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n}\n\n/**\n * Tells a work item's subscribers it moved, at `low` priority. Only sessions\n * already live hear it: a stage change never brings a session online; an\n * offline subscriber sees the stage in its state signal on its next turn.\n */\nexport function createStageChangeNotifier({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n reporting: () => Promise<FactoryReporting | null>;\n}): (change: FactoryStageChange) => Promise<void> {\n return async change => {\n const subscribers = await workItems.listWorkItemSubscribers({\n orgId: change.orgId,\n workItemId: change.workItemId,\n });\n if (subscribers.length === 0) return;\n const resolved = await reporting();\n if (!resolved) return;\n const { label, ref } = await workItemReference(workItems, change);\n const summary = change.fromStage\n ? `${label} moved from ${change.fromStage} to ${change.toStage}.`\n : `${label} moved to ${change.toStage}.`;\n const notification: Omit<SendNotificationSignalInput, 'source'> = {\n kind: 'stage',\n priority: 'low',\n summary,\n payload: { message: summary },\n sourceId: change.workItemId,\n coalesceKey: `factory-stage:${change.workItemId}`,\n attributes: {\n 'work-item': ref,\n ...(change.fromStage ? { 'from-stage': change.fromStage } : {}),\n 'to-stage': change.toStage,\n },\n };\n for (const subscriber of subscribers) {\n try {\n await resolved.deliver({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }, notification, {\n onlyIfLive: true,\n });\n } catch (error) {\n console.warn(`[Factory reports] Unable to tell ${subscriber.sessionId} that ${label} moved.`, error);\n }\n }\n };\n}\n\n/** Reports one run end of a session; the thread is the one the run used. */\nexport type FactorySessionRunEndReporter = (input: {\n sessionId: string;\n threadId: string | null;\n outcome: FactoryRunOutcome;\n since: Date;\n}) => Promise<void>;\n\n/** A tool call a session's run is holding until someone approves or declines it. */\nexport interface FactoryApprovalRequest {\n sessionId: string;\n threadId: string | null;\n toolName: string;\n toolCallId: string;\n args: unknown;\n}\n\nexport type FactoryApprovalNotifier = (request: FactoryApprovalRequest) => Promise<void>;\n\ninterface SessionReportDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<\n WorkItemsStorage,\n 'findActiveRunBindingForSession' | 'findRunBindingBySession' | 'listWorkItemSubscribers' | 'getForProject'\n >;\n reporting: () => Promise<FactoryReporting | null>;\n}\n\n/** How much of a gated call's arguments an approval notice quotes. */\nconst APPROVAL_ARGS_LENGTH = 3000;\n\n/**\n * Who hears from a session: a work session (one with an active run binding)\n * reports to its item's followers, a side session to the session that started\n * it. Any other session (a person's own) has nobody to tell.\n */\nasync function sessionAudience(\n { sessions, workItems }: Omit<SessionReportDependencies, 'reporting'>,\n sessionId: string,\n threadId: string | null,\n /** When the run started: a binding revoked since then (the run moved its card to a finished stage) still counts. */\n since?: Date,\n): Promise<\n | {\n kind: 'work-item';\n binding: NonNullable<Awaited<ReturnType<WorkItemsStorage['findActiveRunBindingForSession']>>>;\n sender: FactoryReportSender;\n }\n | {\n kind: 'origin';\n sender: FactoryReportSender;\n recipient: FactoryDeliveryTarget;\n subject: string;\n /** Set while the origin has muted this side session's reports (approval notices still go through). */\n mutedAt: Date | null;\n }\n | null\n> {\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n let binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId: row.sessionId,\n });\n if (!binding && since && threadId) {\n // The binding records the memory resource the run used: the Factory's, its\n // own id for a run kept on its own resource, or its owner's for a person's\n // session the run started in.\n for (const resourceId of new Set([row.factoryProjectId, row.sessionId, row.userId])) {\n const latest = await workItems.findRunBindingBySession({\n factoryProjectId: row.factoryProjectId,\n threadId,\n resourceId,\n sessionId: row.sessionId,\n });\n if (latest?.orgId === row.orgId && latest.revokedAt && latest.revokedAt >= since) {\n binding = latest;\n break;\n }\n }\n }\n if (binding) {\n return {\n kind: 'work-item',\n binding,\n sender: { sessionId: row.sessionId, threadId: binding.threadId, userId: row.userId },\n };\n }\n if (!row.originSessionId || !row.originThreadId || !threadId) return null;\n return {\n kind: 'origin',\n sender: { sessionId: row.sessionId, threadId, userId: row.userId },\n recipient: { sessionId: row.originSessionId, threadId: row.originThreadId },\n subject: `The side session \"${row.title ?? row.sessionId}\"`,\n mutedAt: row.originMutedAt ?? null,\n };\n}\n\n/** Reports each run end of a work or side session to whoever follows it. */\nexport function createSessionRunEndReporter(deps: SessionReportDependencies): FactorySessionRunEndReporter {\n const workItemRunEnd = createWorkItemRunEndReporter(deps);\n return async ({ sessionId, threadId, outcome, since }) => {\n const audience = await sessionAudience(deps, sessionId, threadId, since);\n if (!audience) return;\n if (audience.kind === 'work-item') {\n const { binding, sender } = audience;\n await workItemRunEnd({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n sender,\n since,\n outcome,\n });\n return;\n }\n // A muted origin still hears when the session is blocked: nothing else would tell it the session is waiting.\n if (audience.mutedAt && outcome.status !== 'blocked') return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n await sendRunEndReport(resolved, {\n sender: audience.sender,\n recipients: [audience.recipient],\n since,\n outcome,\n subject: audience.subject,\n });\n };\n}\n\n/**\n * Tells whoever follows a session, urgently, the moment its run holds a tool\n * call for approval. The run stays open on the gate, so no run end would say\n * so; the notice names the session and call so a follower can answer it with\n * `factory_answer_approval`.\n */\nexport function createSessionApprovalNotifier(deps: SessionReportDependencies): FactoryApprovalNotifier {\n return async ({ sessionId, threadId, toolName, toolCallId, args }) => {\n const audience = await sessionAudience(deps, sessionId, threadId);\n if (!audience) return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n let subject: string;\n let recipients: FactoryDeliveryTarget[];\n let attributes: Record<string, string> = {};\n if (audience.kind === 'work-item') {\n const { binding } = audience;\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: binding.orgId,\n workItemId: binding.workItemId,\n senderSessionId: audience.sender.sessionId,\n });\n const named = await workItemReportAttributes(deps.workItems, binding);\n subject = `The ${binding.role} session for ${named.label}`;\n attributes = named.attributes;\n } else {\n recipients = [audience.recipient];\n subject = audience.subject;\n }\n if (recipients.length === 0) return;\n const argsText = JSON.stringify(args ?? {}) ?? '{}';\n const quoted = argsText.length > APPROVAL_ARGS_LENGTH ? `${argsText.slice(0, APPROVAL_ARGS_LENGTH)}…` : argsText;\n const message = [\n `${subject} is waiting for approval to run ${toolName} with ${quoted}.`,\n `Answer it with factory_answer_approval (sessionId \"${audience.sender.sessionId}\", toolCallId \"${toolCallId}\"), or open that session to approve or decline it there.`,\n ].join(' ');\n const notification = {\n ...reportNotification({\n sender: audience.sender,\n message,\n attributes: { ...attributes, 'waiting-for': 'approval', 'tool-name': toolName, 'tool-call-id': toolCallId },\n }),\n kind: 'approval',\n priority: 'urgent' as const,\n // One notice per held call, even if the gate is announced again.\n dedupeKey: `factory-approval:${toolCallId}`,\n };\n for (const recipient of recipients) await resolved.deliver(recipient, notification);\n };\n}\n"],"mappings":"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"}
|
|
@@ -21,6 +21,8 @@ export type SessionApprovalReport = (request: {
|
|
|
21
21
|
toolCallId: string;
|
|
22
22
|
args: unknown;
|
|
23
23
|
}) => Promise<void>;
|
|
24
|
+
/** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */
|
|
25
|
+
export declare function usageLimitReason(error: unknown): string | undefined;
|
|
24
26
|
/**
|
|
25
27
|
* Report every run end of a session, however the run started: the board, a
|
|
26
28
|
* typed message, a wake, or a run after a restart. Attached to each Factory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-watch.d.ts","sourceRoot":"","sources":["../../src/tools/run-watch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,KAAK,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEnF,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,sFAAsF;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"run-watch.d.ts","sourceRoot":"","sources":["../../src/tools/run-watch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,KAAK,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEnF,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,sFAAsF;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAmBxH,8GAA8G;AAC9G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqCnE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,mBAAmB,EAC3B,EAAE,mBAAmB,EAAE,GAAE;IAAE,mBAAmB,CAAC,EAAE,qBAAqB,CAAA;CAAO,GAC5E,MAAM,IAAI,CAyCZ;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,MAAM,GAChB;IAAE,SAAS,EAAE,MAAM,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAqBrD;AAED,yFAAyF;AACzF,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,GAAG,SAAS,EAC7D,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI,CAqB1B"}
|
package/dist/tools/run-watch.js
CHANGED
|
@@ -3,6 +3,46 @@ import { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from "../session/r
|
|
|
3
3
|
/** How much of a run's error message a `failed` report quotes. */
|
|
4
4
|
const ERROR_MESSAGE_LENGTH = 400;
|
|
5
5
|
/**
|
|
6
|
+
* A usage or quota rejection, not a rate limit or a request-size limit. The
|
|
7
|
+
* wording is the same set account rotation uses, so a provider that reports
|
|
8
|
+
* the cap as a plain 400 still counts.
|
|
9
|
+
*/
|
|
10
|
+
const USAGE_LIMIT_MESSAGE_PATTERN = /usage limit|usage cap|insufficient_quota|quota exceeded|quota exhausted|quota reached|exceeded your current quota|insufficient balance|insufficient credits|credits exhausted|rate limit exceeded|rate limit reached|weekly limit|monthly limit/i;
|
|
11
|
+
const ERROR_CHAIN_DEPTH = 5;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null;
|
|
14
|
+
}
|
|
15
|
+
/** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */
|
|
16
|
+
function usageLimitReason(error) {
|
|
17
|
+
const visited = /* @__PURE__ */ new Set();
|
|
18
|
+
const found = [];
|
|
19
|
+
const add = (value) => {
|
|
20
|
+
if (typeof value !== "string") return;
|
|
21
|
+
const text = value.trim();
|
|
22
|
+
if (text && USAGE_LIMIT_MESSAGE_PATTERN.test(text)) found.push(text);
|
|
23
|
+
};
|
|
24
|
+
function walk(value, depth) {
|
|
25
|
+
if (depth > ERROR_CHAIN_DEPTH || !isRecord(value) || visited.has(value)) return;
|
|
26
|
+
visited.add(value);
|
|
27
|
+
add(value.message);
|
|
28
|
+
if (typeof value.responseBody === "string") try {
|
|
29
|
+
const body = JSON.parse(value.responseBody);
|
|
30
|
+
if (isRecord(body)) {
|
|
31
|
+
add(body.message);
|
|
32
|
+
add(body.detail);
|
|
33
|
+
if (isRecord(body.error)) add(body.error.message);
|
|
34
|
+
else add(body.error);
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
if (value.cause !== void 0) walk(value.cause, depth + 1);
|
|
38
|
+
if (value.error !== void 0) walk(value.error, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
walk(error, 0);
|
|
42
|
+
} catch {}
|
|
43
|
+
return (found.find((text) => text !== "Bad Request") ?? found[0])?.slice(0, ERROR_MESSAGE_LENGTH);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
6
46
|
* Report every run end of a session, however the run started: the board, a
|
|
7
47
|
* typed message, a wake, or a run after a restart. Attached to each Factory
|
|
8
48
|
* session when it is created, so it outlives any one start. A run someone
|
|
@@ -31,7 +71,7 @@ function observeSessionRunReports(session, report, { onApprovalRequested } = {})
|
|
|
31
71
|
return;
|
|
32
72
|
}
|
|
33
73
|
if (event.type === "error") {
|
|
34
|
-
lastError = event.error?.message;
|
|
74
|
+
lastError = usageLimitReason(event.error) ?? event.error?.message;
|
|
35
75
|
return;
|
|
36
76
|
}
|
|
37
77
|
if (event.type === "tool_suspension_cancelled" && parked?.toolCallId === event.toolCallId) {
|
|
@@ -97,6 +137,6 @@ function runOutcome(reason, parked, errorMessage) {
|
|
|
97
137
|
}
|
|
98
138
|
}
|
|
99
139
|
//#endregion
|
|
100
|
-
export { heartbeatOpenRunUntilEnd, observeSessionRunReports, runOutcome };
|
|
140
|
+
export { heartbeatOpenRunUntilEnd, observeSessionRunReports, runOutcome, usageLimitReason };
|
|
101
141
|
|
|
102
142
|
//# sourceMappingURL=run-watch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-watch.js","names":[],"sources":["../../src/tools/run-watch.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from '../session/run-audit.js';\nimport type { RunEndCaptureSession } from '../session/run-audit.js';\nimport type { FactoryRunOutcome } from './reports.js';\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\n\n/** What a run stopped on: a tool waiting for an answer, or one waiting for approval. */\nexport interface ParkedTool {\n toolName: string;\n toolCallId: string;\n approval: boolean;\n}\n\nexport interface RunReportSession {\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\n/** Reports one run's end; `since` is when the run's window opened. Should not throw. */\nexport type SessionRunEndReport = (outcome: FactoryRunOutcome, since: Date) => Promise<void>;\n\n/** Told when the run holds a tool call for approval; the run stays open meanwhile. */\nexport type SessionApprovalReport = (request: { toolName: string; toolCallId: string; args: unknown }) => Promise<void>;\n\n/** How much of a run's error message a `failed` report quotes. */\nconst ERROR_MESSAGE_LENGTH = 400;\n\n/**\n * Report every run end of a session, however the run started: the board, a\n * typed message, a wake, or a run after a restart. Attached to each Factory\n * session when it is created, so it outlives any one start. A run someone\n * stopped or steered says nothing, and its window carries into the next run,\n * so reports the agent made in it still count.\n */\nexport function observeSessionRunReports(\n session: RunReportSession,\n report: SessionRunEndReport,\n { onApprovalRequested }: { onApprovalRequested?: SessionApprovalReport } = {},\n): () => void {\n let parked: ParkedTool | undefined;\n let lastError: string | undefined;\n let since = new Date();\n const settle = (work: Promise<void>) =>\n void work.catch(error => {\n console.warn('[Factory reports] Unable to report on a session run.', error);\n });\n return session.subscribe(event => {\n if (event.type === 'tool_suspended' || event.type === 'tool_approval_required') {\n parked = {\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n approval: event.type === 'tool_approval_required',\n };\n // An approval gate holds the run open, so it is announced now, not at the run's end.\n if (event.type === 'tool_approval_required' && onApprovalRequested) {\n settle(onApprovalRequested({ toolName: event.toolName, toolCallId: event.toolCallId, args: event.args }));\n }\n return;\n }\n if (event.type === 'error') {\n lastError = event.error?.message;\n return;\n }\n if (event.type === 'tool_suspension_cancelled' && parked?.toolCallId === event.toolCallId) {\n parked = undefined;\n return;\n }\n if (event.type !== 'agent_end') return;\n const outcome = runOutcome(event.reason, parked, lastError);\n parked = undefined;\n lastError = undefined;\n if (!outcome) return;\n // Move the window now, so a run that starts before this report settles is judged on its own;\n // reports made up to the end, including its last millisecond, belong to the run that ended.\n const runSince = since;\n since = new Date(Date.now() + 1);\n settle(report(outcome, runSince));\n });\n}\n\n/**\n * Renew a kicked-off run's open-run entry until the run ends, like the\n * dispatcher's heartbeat, so no dispatcher starts the role again meanwhile.\n * `endReason` is how that run ended, once it has; `stop` is for a kickoff that fails.\n */\nexport function heartbeatOpenRunUntilEnd(\n session: RunEndCaptureSession,\n kickoffId: string,\n): { endReason: () => RunEndReason; stop: () => void } {\n let endReason: RunEndReason;\n let unsubscribe = () => {};\n const timer = setInterval(\n () => void heartbeatSessionOpenRun(session, kickoffId),\n Math.floor(FACTORY_OPEN_RUN_STALE_MS / 3),\n );\n timer.unref?.();\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n unsubscribe();\n };\n unsubscribe = session.subscribe(event => {\n if (event.type !== 'agent_end') return;\n endReason ??= event.reason;\n stop();\n });\n return { endReason: () => endReason, stop };\n}\n\n/** `null` for an aborted run: someone stopped or steered it, which is not an outcome. */\nexport function runOutcome(\n reason: RunEndReason,\n parked: Pick<ParkedTool, 'toolName' | 'approval'> | undefined,\n errorMessage?: string,\n): FactoryRunOutcome | null {\n switch (reason) {\n case 'error': {\n const message = errorMessage?.trim();\n return {\n status: 'failed',\n reason: message\n ? `the run ended in error: ${message.slice(0, ERROR_MESSAGE_LENGTH)}`\n : 'the run ended in error.',\n };\n }\n case 'aborted':\n return null;\n case 'suspended':\n return {\n status: 'blocked',\n ...(parked ? { toolName: parked.toolName, ...(parked.approval ? { approval: true } : {}) } : {}),\n };\n default:\n return { status: 'done' };\n }\n}\n"],"mappings":";;;AA0BA,MAAM,uBAAuB;;;;;;;;
|
|
1
|
+
{"version":3,"file":"run-watch.js","names":[],"sources":["../../src/tools/run-watch.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from '../session/run-audit.js';\nimport type { RunEndCaptureSession } from '../session/run-audit.js';\nimport type { FactoryRunOutcome } from './reports.js';\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\n\n/** What a run stopped on: a tool waiting for an answer, or one waiting for approval. */\nexport interface ParkedTool {\n toolName: string;\n toolCallId: string;\n approval: boolean;\n}\n\nexport interface RunReportSession {\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\n/** Reports one run's end; `since` is when the run's window opened. Should not throw. */\nexport type SessionRunEndReport = (outcome: FactoryRunOutcome, since: Date) => Promise<void>;\n\n/** Told when the run holds a tool call for approval; the run stays open meanwhile. */\nexport type SessionApprovalReport = (request: { toolName: string; toolCallId: string; args: unknown }) => Promise<void>;\n\n/** How much of a run's error message a `failed` report quotes. */\nconst ERROR_MESSAGE_LENGTH = 400;\n\n/**\n * A usage or quota rejection, not a rate limit or a request-size limit. The\n * wording is the same set account rotation uses, so a provider that reports\n * the cap as a plain 400 still counts.\n */\nconst USAGE_LIMIT_MESSAGE_PATTERN =\n /usage limit|usage cap|insufficient_quota|quota exceeded|quota exhausted|quota reached|exceeded your current quota|insufficient balance|insufficient credits|credits exhausted|rate limit exceeded|rate limit reached|weekly limit|monthly limit/i;\n\nconst ERROR_CHAIN_DEPTH = 5;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */\nexport function usageLimitReason(error: unknown): string | undefined {\n const visited = new Set<object>();\n const found: string[] = [];\n const add = (value: unknown) => {\n if (typeof value !== 'string') return;\n const text = value.trim();\n if (text && USAGE_LIMIT_MESSAGE_PATTERN.test(text)) found.push(text);\n };\n\n function walk(value: unknown, depth: number) {\n if (depth > ERROR_CHAIN_DEPTH || !isRecord(value) || visited.has(value)) return;\n visited.add(value);\n add(value.message);\n if (typeof value.responseBody === 'string') {\n try {\n const body: unknown = JSON.parse(value.responseBody);\n if (isRecord(body)) {\n add(body.message);\n add(body.detail);\n if (isRecord(body.error)) add(body.error.message);\n else add(body.error);\n }\n } catch {\n // A non-JSON body can echo the request. Leave it out.\n }\n }\n if (value.cause !== undefined) walk(value.cause, depth + 1);\n if (value.error !== undefined) walk(value.error, depth + 1);\n }\n\n try {\n walk(error, 0);\n } catch {\n // A malformed error object still reports whatever was already found.\n }\n const reason = found.find(text => text !== 'Bad Request') ?? found[0];\n return reason?.slice(0, ERROR_MESSAGE_LENGTH);\n}\n\n/**\n * Report every run end of a session, however the run started: the board, a\n * typed message, a wake, or a run after a restart. Attached to each Factory\n * session when it is created, so it outlives any one start. A run someone\n * stopped or steered says nothing, and its window carries into the next run,\n * so reports the agent made in it still count.\n */\nexport function observeSessionRunReports(\n session: RunReportSession,\n report: SessionRunEndReport,\n { onApprovalRequested }: { onApprovalRequested?: SessionApprovalReport } = {},\n): () => void {\n let parked: ParkedTool | undefined;\n let lastError: string | undefined;\n let since = new Date();\n const settle = (work: Promise<void>) =>\n void work.catch(error => {\n console.warn('[Factory reports] Unable to report on a session run.', error);\n });\n return session.subscribe(event => {\n if (event.type === 'tool_suspended' || event.type === 'tool_approval_required') {\n parked = {\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n approval: event.type === 'tool_approval_required',\n };\n // An approval gate holds the run open, so it is announced now, not at the run's end.\n if (event.type === 'tool_approval_required' && onApprovalRequested) {\n settle(onApprovalRequested({ toolName: event.toolName, toolCallId: event.toolCallId, args: event.args }));\n }\n return;\n }\n if (event.type === 'error') {\n const quota = usageLimitReason(event.error);\n lastError = quota ?? event.error?.message;\n return;\n }\n if (event.type === 'tool_suspension_cancelled' && parked?.toolCallId === event.toolCallId) {\n parked = undefined;\n return;\n }\n if (event.type !== 'agent_end') return;\n const outcome = runOutcome(event.reason, parked, lastError);\n parked = undefined;\n lastError = undefined;\n if (!outcome) return;\n // Move the window now, so a run that starts before this report settles is judged on its own;\n // reports made up to the end, including its last millisecond, belong to the run that ended.\n const runSince = since;\n since = new Date(Date.now() + 1);\n settle(report(outcome, runSince));\n });\n}\n\n/**\n * Renew a kicked-off run's open-run entry until the run ends, like the\n * dispatcher's heartbeat, so no dispatcher starts the role again meanwhile.\n * `endReason` is how that run ended, once it has; `stop` is for a kickoff that fails.\n */\nexport function heartbeatOpenRunUntilEnd(\n session: RunEndCaptureSession,\n kickoffId: string,\n): { endReason: () => RunEndReason; stop: () => void } {\n let endReason: RunEndReason;\n let unsubscribe = () => {};\n const timer = setInterval(\n () => void heartbeatSessionOpenRun(session, kickoffId),\n Math.floor(FACTORY_OPEN_RUN_STALE_MS / 3),\n );\n timer.unref?.();\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n unsubscribe();\n };\n unsubscribe = session.subscribe(event => {\n if (event.type !== 'agent_end') return;\n endReason ??= event.reason;\n stop();\n });\n return { endReason: () => endReason, stop };\n}\n\n/** `null` for an aborted run: someone stopped or steered it, which is not an outcome. */\nexport function runOutcome(\n reason: RunEndReason,\n parked: Pick<ParkedTool, 'toolName' | 'approval'> | undefined,\n errorMessage?: string,\n): FactoryRunOutcome | null {\n switch (reason) {\n case 'error': {\n const message = errorMessage?.trim();\n return {\n status: 'failed',\n reason: message\n ? `the run ended in error: ${message.slice(0, ERROR_MESSAGE_LENGTH)}`\n : 'the run ended in error.',\n };\n }\n case 'aborted':\n return null;\n case 'suspended':\n return {\n status: 'blocked',\n ...(parked ? { toolName: parked.toolName, ...(parked.approval ? { approval: true } : {}) } : {}),\n };\n default:\n return { status: 'done' };\n }\n}\n"],"mappings":"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"}
|
package/dist/tools/scope.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export interface FactoryToolScope {
|
|
|
13
13
|
factoryProjectId: string;
|
|
14
14
|
/** The signed-in caller for a user session; the session row's owner for a worker. */
|
|
15
15
|
userId: string;
|
|
16
|
-
/** The source-control session id
|
|
16
|
+
/** The source-control session id. */
|
|
17
17
|
sessionId: string;
|
|
18
|
+
/** The memory resource the session runs under. */
|
|
18
19
|
resourceId: string;
|
|
19
20
|
threadId: string;
|
|
20
21
|
/** `worker` sessions are bound to a work item run; only `user` sessions manage the Factory. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/tools/scope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EACL,uBAAuB,EAIxB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,EAAE,uBAAuB,EAAE,CAAC;AAEnC,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE,sGAAsG;AACtG,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,
|
|
1
|
+
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/tools/scope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EACL,uBAAuB,EAIxB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,EAAE,uBAAuB,EAAE,CAAC;AAEnC,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE,sGAAsG;AACtG,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,sGAAsG;IACtG,WAAW,EAAE,OAAO,CAAC;IACrB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,2EAA2E;IAC3E,MAAM,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,cAAc,CAAC;IAC/B,mFAAmF;IACnF,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,EAC5C,cAAc,EACd,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE,8BAA8B,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkEnE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAK3E"}
|
package/dist/tools/scope.js
CHANGED
|
@@ -12,9 +12,10 @@ const FACTORY_SLACK_SENDER_KEY = "mastra__factorySlackSender";
|
|
|
12
12
|
async function resolveFactoryToolScope({ requestContext, authEnabled, sessions, projects, workItems }) {
|
|
13
13
|
if (typeof requestContext?.get !== "function") return null;
|
|
14
14
|
const controller = requestContext.get("controller");
|
|
15
|
+
const sessionId = controller?.sessionId;
|
|
15
16
|
const resourceId = controller?.resourceId;
|
|
16
17
|
const threadId = controller?.threadId;
|
|
17
|
-
if (!resourceId || !threadId) return null;
|
|
18
|
+
if (!sessionId || !resourceId || !threadId) return null;
|
|
18
19
|
let caller = null;
|
|
19
20
|
if (authEnabled) {
|
|
20
21
|
const slackSender = requestContext.get(FACTORY_SLACK_SENDER_KEY);
|
|
@@ -32,7 +33,7 @@ async function resolveFactoryToolScope({ requestContext, authEnabled, sessions,
|
|
|
32
33
|
orgId: "local",
|
|
33
34
|
userId: "local"
|
|
34
35
|
};
|
|
35
|
-
const row = await sessions.getBySessionId(
|
|
36
|
+
const row = await sessions.getBySessionId(sessionId);
|
|
36
37
|
if (!row?.factoryProjectId) return null;
|
|
37
38
|
if (caller && row.orgId !== caller.orgId) return null;
|
|
38
39
|
const origin = row.originSessionId && row.originThreadId ? {
|
package/dist/tools/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.js","names":[],"sources":["../../src/tools/scope.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport {\n FACTORY_INTERACTIVE_KEY,\n factoryUserOrgId,\n getFactoryAuthUserFromContext,\n getFactoryAuthUserId,\n} from '../auth.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { AuditActorProfileInput } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\nexport { FACTORY_INTERACTIVE_KEY };\n\n/** Linked Slack sender, separate from the session owner's model-credential identity. */\nexport const FACTORY_SLACK_SENDER_KEY = 'mastra__factorySlackSender';\n\n/** Who is calling Factory tools, and from which session. Everything a tool needs to stay in scope. */\nexport interface FactoryToolScope {\n orgId: string;\n factoryProjectId: string;\n /** The signed-in caller for a user session; the session row's owner for a worker. */\n userId: string;\n /** The source-control session id
|
|
1
|
+
{"version":3,"file":"scope.js","names":[],"sources":["../../src/tools/scope.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport {\n FACTORY_INTERACTIVE_KEY,\n factoryUserOrgId,\n getFactoryAuthUserFromContext,\n getFactoryAuthUserId,\n} from '../auth.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { AuditActorProfileInput } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\nexport { FACTORY_INTERACTIVE_KEY };\n\n/** Linked Slack sender, separate from the session owner's model-credential identity. */\nexport const FACTORY_SLACK_SENDER_KEY = 'mastra__factorySlackSender';\n\n/** Who is calling Factory tools, and from which session. Everything a tool needs to stay in scope. */\nexport interface FactoryToolScope {\n orgId: string;\n factoryProjectId: string;\n /** The signed-in caller for a user session; the session row's owner for a worker. */\n userId: string;\n /** The source-control session id. */\n sessionId: string;\n /** The memory resource the session runs under. */\n resourceId: string;\n threadId: string;\n /** `worker` sessions are bound to a work item run; only `user` sessions manage the Factory. */\n kind: 'user' | 'worker';\n /** True only for turns a signed-in person started over HTTP (see {@link FACTORY_INTERACTIVE_KEY}). */\n interactive: boolean;\n /** The caller's name and avatar, stamped on audit events so the board can show who acted. */\n actorProfile?: AuditActorProfileInput;\n /** Set for a side session: the user session and thread that started it. */\n origin?: { sessionId: string; threadId: string };\n}\n\nexport interface ResolveFactoryToolScopeOptions {\n requestContext: RequestContext;\n /** `routeAuth.enabled()`; when false every caller is the shared `local` tenant. */\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n projects: Pick<FactoryProjectsStorage, 'get'>;\n workItems: Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n}\n\n/**\n * Resolve the Factory a session's agent may act in. The scope comes from the\n * stored source-control session row and the authenticated caller, never from\n * the `factoryProjectId` thread tag alone: the browser sets that tag and\n * nothing clears it.\n */\nexport async function resolveFactoryToolScope({\n requestContext,\n authEnabled,\n sessions,\n projects,\n workItems,\n}: ResolveFactoryToolScopeOptions): Promise<FactoryToolScope | null> {\n if (typeof requestContext?.get !== 'function') return null;\n const controller = requestContext.get('controller') as AgentControllerRequestContext<unknown> | undefined;\n const sessionId = controller?.sessionId;\n const resourceId = controller?.resourceId;\n const threadId = controller?.threadId;\n if (!sessionId || !resourceId || !threadId) return null;\n\n let caller: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput } | null = null;\n if (authEnabled) {\n const slackSender = requestContext.get(FACTORY_SLACK_SENDER_KEY) as { userId: string; orgId: string } | undefined;\n const user = getFactoryAuthUserFromContext(requestContext);\n if (slackSender && factoryUserOrgId(user) !== slackSender.orgId) return null;\n const userId = slackSender?.userId ?? getFactoryAuthUserId(user);\n const orgId = slackSender?.orgId ?? factoryUserOrgId(user);\n const actorProfile = slackSender ? undefined : auditActorProfile(user);\n if (userId && orgId) caller = { orgId, userId, ...(actorProfile ? { actorProfile } : {}) };\n } else {\n caller = { orgId: 'local', userId: 'local' };\n }\n\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n // A caller from another org never acts here, worker or not.\n if (caller && row.orgId !== caller.orgId) return null;\n\n // A side session works for the session that started it. Otherwise any\n // binding, active or revoked, on any thread: a session that ever ran a role stays a worker.\n const origin =\n row.originSessionId && row.originThreadId\n ? { sessionId: row.originSessionId, threadId: row.originThreadId }\n : undefined;\n const worker =\n origin !== undefined ||\n (await workItems.hasRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId: row.sessionId,\n }));\n\n // A worker acts as its session row: dispatcher-driven runs carry no signed-in user.\n // A user session acts as the signed-in caller, who must be able to see it.\n let identity: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput };\n if (worker) {\n identity = { orgId: row.orgId, userId: row.userId };\n } else {\n if (!caller) return null;\n if (row.visibility === 'private' && row.userId !== caller.userId) return null;\n identity = caller;\n }\n\n const project = await projects.get({ orgId: identity.orgId, id: row.factoryProjectId });\n if (!project) return null;\n\n return {\n orgId: identity.orgId,\n factoryProjectId: row.factoryProjectId,\n userId: identity.userId,\n sessionId: row.sessionId,\n resourceId,\n threadId,\n kind: worker ? 'worker' : 'user',\n interactive: requestContext.get(FACTORY_INTERACTIVE_KEY) === true,\n ...(identity.actorProfile ? { actorProfile: identity.actorProfile } : {}),\n ...(origin ? { origin } : {}),\n };\n}\n\n/**\n * Refuses an action only a signed-in person may take. Turns nobody signed in\n * started (notification wakes, webhooks, dispatcher notices) carry text other\n * people wrote, so they can't approve, resolve proposals, or waive plan approval.\n */\nexport function requirePerson(scope: FactoryToolScope, action: string): void {\n if (scope.interactive) return;\n throw new Error(\n `Only a person can ${action}. Nobody signed in started this turn (it came from a notification, webhook or rule); ask the person to do it from the board or by messaging this session.`,\n );\n}\n"],"mappings":";;;;AAkBA,MAAa,2BAA2B;;;;;;;AAsCxC,eAAsB,wBAAwB,EAC5C,gBACA,aACA,UACA,UACA,aACmE;CACnE,IAAI,OAAO,gBAAgB,QAAQ,YAAY,OAAO;CACtD,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,MAAM,YAAY,YAAY;CAC9B,MAAM,aAAa,YAAY;CAC/B,MAAM,WAAW,YAAY;CAC7B,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,UAAU,OAAO;CAEnD,IAAI,SAA0F;CAC9F,IAAI,aAAa;EACf,MAAM,cAAc,eAAe,IAAI,wBAAwB;EAC/D,MAAM,OAAO,8BAA8B,cAAc;EACzD,IAAI,eAAe,iBAAiB,IAAI,MAAM,YAAY,OAAO,OAAO;EACxE,MAAM,SAAS,aAAa,UAAU,qBAAqB,IAAI;EAC/D,MAAM,QAAQ,aAAa,SAAS,iBAAiB,IAAI;EACzD,MAAM,eAAe,cAAc,KAAA,IAAY,kBAAkB,IAAI;EACrE,IAAI,UAAU,OAAO,SAAS;GAAE;GAAO;GAAQ,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EAAG;CAC3F,OACE,SAAS;EAAE,OAAO;EAAS,QAAQ;CAAQ;CAG7C,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,CAAC,KAAK,kBAAkB,OAAO;CAEnC,IAAI,UAAU,IAAI,UAAU,OAAO,OAAO,OAAO;CAIjD,MAAM,SACJ,IAAI,mBAAmB,IAAI,iBACvB;EAAE,WAAW,IAAI;EAAiB,UAAU,IAAI;CAAe,IAC/D,KAAA;CACN,MAAM,SACJ,WAAW,KAAA,KACV,MAAM,UAAU,wBAAwB;EACvC,OAAO,IAAI;EACX,kBAAkB,IAAI;EACtB,WAAW,IAAI;CACjB,CAAC;CAIH,IAAI;CACJ,IAAI,QACF,WAAW;EAAE,OAAO,IAAI;EAAO,QAAQ,IAAI;CAAO;MAC7C;EACL,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,OAAO,QAAQ,OAAO;EACzE,WAAW;CACb;CAGA,IAAI,CAAC,MADiB,SAAS,IAAI;EAAE,OAAO,SAAS;EAAO,IAAI,IAAI;CAAiB,CAAC,GACxE,OAAO;CAErB,OAAO;EACL,OAAO,SAAS;EAChB,kBAAkB,IAAI;EACtB,QAAQ,SAAS;EACjB,WAAW,IAAI;EACf;EACA;EACA,MAAM,SAAS,WAAW;EAC1B,aAAa,eAAe,IAAI,uBAAuB,MAAM;EAC7D,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAa,IAAI,CAAC;EACvE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC7B;AACF;;;;;;AAOA,SAAgB,cAAc,OAAyB,QAAsB;CAC3E,IAAI,MAAM,aAAa;CACvB,MAAM,IAAI,MACR,qBAAqB,OAAO,0JAC9B;AACF"}
|
|
@@ -31,7 +31,7 @@ export interface FactorySessionToolDependencies {
|
|
|
31
31
|
};
|
|
32
32
|
/** Resolved lazily: the controller only exists once the mount is up. */
|
|
33
33
|
messaging: () => Promise<FactoryMessaging | null>;
|
|
34
|
-
workItems: Pick<WorkItemsStorage, 'list' | 'getForProject' | 'listRunBindings' | 'findActiveRunBindingForSession' | 'findRunBindingByThread' | 'subscribeToWorkItem' | 'unsubscribeFromWorkItem' | 'listSessionSubscriptions' | 'listWorkItemSubscribers'>;
|
|
34
|
+
workItems: Pick<WorkItemsStorage, 'list' | 'getForProject' | 'listRunBindings' | 'findActiveRunBindingForSession' | 'findRunBindingByThread' | 'hasRunBindingForSession' | 'subscribeToWorkItem' | 'unsubscribeFromWorkItem' | 'listSessionSubscriptions' | 'listWorkItemSubscribers'>;
|
|
35
35
|
boards: BoardRegistry;
|
|
36
36
|
auditRecorder: AuditRecorder;
|
|
37
37
|
intake?: Pick<IntakeStorage, 'getConfig'>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA8alB"}
|
|
@@ -5,6 +5,7 @@ import { effectiveModelScope } from "../storage/domains/projects/base.js";
|
|
|
5
5
|
import { ensureFactorySourceSession, hydrateFactorySession, resolveFactorySessionModel } from "../session/factory-session.js";
|
|
6
6
|
import { resolveSessionThread } from "./deliver.js";
|
|
7
7
|
import { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from "./read-tools.js";
|
|
8
|
+
import { sessionModel } from "./side-sessions.js";
|
|
8
9
|
import { resolveFactoryModelPolicy } from "../session/credential-policy.js";
|
|
9
10
|
import { buildModelCatalog, listSessionScopedCredentials } from "../routes/config.js";
|
|
10
11
|
import { USER_SESSION_BRANCH_PREFIX } from "../routes/source-control-sessions.js";
|
|
@@ -38,7 +39,7 @@ function titleFrom(prompt) {
|
|
|
38
39
|
/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */
|
|
39
40
|
function createFactorySessionTools(scope, deps) {
|
|
40
41
|
/** The models a session can switch to, resolved from the target session's owner and project policy. */
|
|
41
|
-
async function sessionModelCatalog(controller, sessionId, threadId) {
|
|
42
|
+
async function sessionModelCatalog(controller, sessionId, threadId, resourceId) {
|
|
42
43
|
const models = await controller.listAvailableModels();
|
|
43
44
|
const known = new Set(models.flatMap((model) => typeof model.id === "string" ? [model.id] : []));
|
|
44
45
|
const tenantCatalog = deps.sessionModel.tenantCatalog;
|
|
@@ -53,7 +54,8 @@ function createFactorySessionTools(scope, deps) {
|
|
|
53
54
|
organizationId: scope.orgId
|
|
54
55
|
});
|
|
55
56
|
requestContext.set("controller", {
|
|
56
|
-
|
|
57
|
+
sessionId,
|
|
58
|
+
resourceId,
|
|
57
59
|
threadId
|
|
58
60
|
});
|
|
59
61
|
const policy = await resolveFactoryModelPolicy(requestContext, {
|
|
@@ -86,6 +88,10 @@ function createFactorySessionTools(scope, deps) {
|
|
|
86
88
|
scopeLabel: modelScope === "factory" ? "Factory" : "session owner"
|
|
87
89
|
};
|
|
88
90
|
}
|
|
91
|
+
function unrunnableModelError(modelId, catalog) {
|
|
92
|
+
const options = catalog.usable.length ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(", ")}${catalog.usable.length > LISTED_MODELS ? ", …" : ""}.` : " No models are available to it; connect a provider credential first.";
|
|
93
|
+
return /* @__PURE__ */ new Error(catalog.known.has(modelId) ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}` : `Unknown model ${modelId}.${options}`);
|
|
94
|
+
}
|
|
89
95
|
const switchModel = createTool({
|
|
90
96
|
id: "factory_switch_session_model",
|
|
91
97
|
description: "Switch the model of a session you own in this Factory (side, work item, or review session) without recreating it. A running session switches immediately; a stopped one uses the model on its next run. The model must be one the session credentials can run: the owner credentials for a user-scoped session, the Factory credentials for a Factory-scoped one. Use a session ID from the Factory session or work item tools.",
|
|
@@ -106,13 +112,11 @@ function createFactorySessionTools(scope, deps) {
|
|
|
106
112
|
sessionId
|
|
107
113
|
});
|
|
108
114
|
if (!threadId) throw new Error("Session has no conversation thread yet.");
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new Error(catalog.known.has(modelId) ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}` : `Unknown model ${modelId}.${options}`);
|
|
113
|
-
}
|
|
115
|
+
const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;
|
|
116
|
+
const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);
|
|
117
|
+
if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);
|
|
114
118
|
const { live } = await messaging.controller.setThreadModel({
|
|
115
|
-
resourceId
|
|
119
|
+
resourceId,
|
|
116
120
|
threadId,
|
|
117
121
|
modelId
|
|
118
122
|
});
|
|
@@ -128,6 +132,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
128
132
|
id: "factory_start_session",
|
|
129
133
|
description: [
|
|
130
134
|
"Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.",
|
|
135
|
+
"A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.",
|
|
131
136
|
"Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.",
|
|
132
137
|
"With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.",
|
|
133
138
|
"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board."
|
|
@@ -135,13 +140,14 @@ function createFactorySessionTools(scope, deps) {
|
|
|
135
140
|
inputSchema: z.object({
|
|
136
141
|
prompt: z.string().trim().min(1).max(8e3).describe("The first message the new session receives."),
|
|
137
142
|
title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),
|
|
143
|
+
model: z.string().trim().min(1).max(200).optional().describe("Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it."),
|
|
138
144
|
workItem: workItemRefSchema.optional(),
|
|
139
145
|
role: z.string().trim().min(1).max(64).optional()
|
|
140
146
|
}).strict().refine((input) => input.role === void 0 || input.workItem !== void 0, {
|
|
141
147
|
message: "Pass workItem together with role.",
|
|
142
148
|
path: ["role"]
|
|
143
149
|
}),
|
|
144
|
-
execute: async ({ prompt, title, workItem, role }) => {
|
|
150
|
+
execute: async ({ prompt, title, model, workItem, role }) => {
|
|
145
151
|
if (workItem !== void 0) {
|
|
146
152
|
if (!scope.interactive) throw new Error("Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.");
|
|
147
153
|
return startWorkItemRole({
|
|
@@ -156,6 +162,17 @@ function createFactorySessionTools(scope, deps) {
|
|
|
156
162
|
if (!parent || !sourceControl) throw new Error("This session has no Factory session row to start from.");
|
|
157
163
|
const messaging = await deps.messaging();
|
|
158
164
|
if (!messaging) throw new Error("Starting sessions is not available on this server.");
|
|
165
|
+
const parentThreadId = await resolveSessionThread({
|
|
166
|
+
controller: messaging.controller,
|
|
167
|
+
sessions: deps.sessions,
|
|
168
|
+
workItems: deps.workItems,
|
|
169
|
+
sessionId: scope.sessionId
|
|
170
|
+
});
|
|
171
|
+
const parentResourceId = parentThreadId ? (await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId : scope.sessionId;
|
|
172
|
+
const catalog = await sessionModelCatalog(messaging.controller, scope.sessionId, parentThreadId ?? scope.threadId, parentResourceId);
|
|
173
|
+
const inherited = model ? null : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);
|
|
174
|
+
const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : void 0);
|
|
175
|
+
if (requestedModel && !catalog.usable.includes(requestedModel)) throw unrunnableModelError(requestedModel, catalog);
|
|
159
176
|
const sessionId = globalThis.crypto.randomUUID();
|
|
160
177
|
const ensured = await ensureFactorySourceSession({
|
|
161
178
|
sourceControl,
|
|
@@ -171,7 +188,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
171
188
|
threadId: scope.threadId
|
|
172
189
|
}
|
|
173
190
|
});
|
|
174
|
-
const
|
|
191
|
+
const resolvedModel = await resolveFactorySessionModel({
|
|
175
192
|
...deps.sessionModel,
|
|
176
193
|
factoryProjectId: scope.factoryProjectId,
|
|
177
194
|
orgId: scope.orgId,
|
|
@@ -190,7 +207,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
190
207
|
const session = await messaging.controller.createSession({
|
|
191
208
|
id: ensured.sessionId,
|
|
192
209
|
ownerId: scope.userId,
|
|
193
|
-
resourceId:
|
|
210
|
+
resourceId: parent.visibility === "private" ? scope.userId : scope.factoryProjectId,
|
|
194
211
|
threadId: ensured.sessionId,
|
|
195
212
|
requestContext: kickoffContext,
|
|
196
213
|
tags
|
|
@@ -198,9 +215,9 @@ function createFactorySessionTools(scope, deps) {
|
|
|
198
215
|
await session.state.set(tags);
|
|
199
216
|
await hydrateFactorySession(session, {
|
|
200
217
|
orgId: scope.orgId,
|
|
201
|
-
modelScope:
|
|
218
|
+
modelScope: resolvedModel.modelScope,
|
|
202
219
|
ownerUserId: scope.userId,
|
|
203
|
-
defaultModelId:
|
|
220
|
+
defaultModelId: requestedModel ?? resolvedModel.modelId
|
|
204
221
|
});
|
|
205
222
|
const threadId = session.thread.requireId();
|
|
206
223
|
const threadTitle = title ?? titleFrom(prompt);
|
|
@@ -219,7 +236,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
219
236
|
await primeTenantCredentialsForTenant({
|
|
220
237
|
orgId: scope.orgId,
|
|
221
238
|
userId: scope.userId,
|
|
222
|
-
credentialScope:
|
|
239
|
+
credentialScope: resolvedModel.modelScope
|
|
223
240
|
});
|
|
224
241
|
await session.sendSignal({
|
|
225
242
|
id: globalThis.crypto.randomUUID(),
|
|
@@ -269,7 +286,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
269
286
|
branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,
|
|
270
287
|
projectRepositoryId,
|
|
271
288
|
attributeToUserId: scope.userId,
|
|
272
|
-
visibility:
|
|
289
|
+
visibility: "org"
|
|
273
290
|
});
|
|
274
291
|
const followScope = {
|
|
275
292
|
orgId: scope.orgId,
|
|
@@ -324,7 +341,7 @@ function createFactorySessionTools(scope, deps) {
|
|
|
324
341
|
throw error;
|
|
325
342
|
}
|
|
326
343
|
const replaced = (await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id)).find((binding) => binding.role === role && binding.id !== prepared.bindingId && binding.revokedAt !== null && binding.revokedAt >= startedAt);
|
|
327
|
-
const session = await messaging.controller.
|
|
344
|
+
const session = await messaging.controller.getSession(prepared.sessionId);
|
|
328
345
|
if (!session) throw new Error(`The ${role} session was bound but is not running; open it from the board.`);
|
|
329
346
|
const kickoffId = globalThis.crypto.randomUUID();
|
|
330
347
|
const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);
|