@qvac/core 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +198 -0
- package/NOTICE.md +13 -0
- package/README.md +694 -0
- package/SECURITY.md +6 -0
- package/dist/assistant.bundle +47256 -0
- package/dist/assistant.cjs +18 -0
- package/dist/assistant.d.ts +8 -0
- package/dist/prebuilds-manifest.json +380 -0
- package/dist-lib/client.d.ts +2 -0
- package/dist-lib/client.js +1 -0
- package/dist-lib/index.d.ts +13 -0
- package/dist-lib/index.js +10 -0
- package/dist-lib/lib/agent-icons.d.ts +3 -0
- package/dist-lib/lib/agent-icons.js +1 -0
- package/dist-lib/lib/api/agents.d.ts +5 -0
- package/dist-lib/lib/api/agents.js +38 -0
- package/dist-lib/lib/api/app-config.d.ts +5 -0
- package/dist-lib/lib/api/app-config.js +108 -0
- package/dist-lib/lib/api/approval-rules.d.ts +3 -0
- package/dist-lib/lib/api/approval-rules.js +9 -0
- package/dist-lib/lib/api/attachments.d.ts +8 -0
- package/dist-lib/lib/api/attachments.js +15 -0
- package/dist-lib/lib/api/chat-groups.d.ts +5 -0
- package/dist-lib/lib/api/chat-groups.js +12 -0
- package/dist-lib/lib/api/chat.d.ts +5 -0
- package/dist-lib/lib/api/chat.js +38 -0
- package/dist-lib/lib/api/chunks.d.ts +5 -0
- package/dist-lib/lib/api/chunks.js +61 -0
- package/dist-lib/lib/api/config.d.ts +5 -0
- package/dist-lib/lib/api/config.js +15 -0
- package/dist-lib/lib/api/current-mesh.d.ts +5 -0
- package/dist-lib/lib/api/current-mesh.js +12 -0
- package/dist-lib/lib/api/custom-models.d.ts +5 -0
- package/dist-lib/lib/api/custom-models.js +45 -0
- package/dist-lib/lib/api/devices.d.ts +8 -0
- package/dist-lib/lib/api/devices.js +125 -0
- package/dist-lib/lib/api/diagnostics.d.ts +7 -0
- package/dist-lib/lib/api/diagnostics.js +7 -0
- package/dist-lib/lib/api/effective-ctx-size.d.ts +2 -0
- package/dist-lib/lib/api/effective-ctx-size.js +10 -0
- package/dist-lib/lib/api/execution.d.ts +3 -0
- package/dist-lib/lib/api/execution.js +45 -0
- package/dist-lib/lib/api/files.d.ts +3 -0
- package/dist-lib/lib/api/files.js +48 -0
- package/dist-lib/lib/api/index.d.ts +62 -0
- package/dist-lib/lib/api/index.js +172 -0
- package/dist-lib/lib/api/instrument.d.ts +2 -0
- package/dist-lib/lib/api/instrument.js +86 -0
- package/dist-lib/lib/api/knowledge.d.ts +8 -0
- package/dist-lib/lib/api/knowledge.js +26 -0
- package/dist-lib/lib/api/logs.d.ts +14 -0
- package/dist-lib/lib/api/logs.js +116 -0
- package/dist-lib/lib/api/mesh.d.ts +13 -0
- package/dist-lib/lib/api/mesh.js +25 -0
- package/dist-lib/lib/api/model-download.d.ts +5 -0
- package/dist-lib/lib/api/model-download.js +47 -0
- package/dist-lib/lib/api/network.d.ts +8 -0
- package/dist-lib/lib/api/network.js +6 -0
- package/dist-lib/lib/api/notifications.d.ts +21 -0
- package/dist-lib/lib/api/notifications.js +108 -0
- package/dist-lib/lib/api/rag.d.ts +7 -0
- package/dist-lib/lib/api/rag.js +216 -0
- package/dist-lib/lib/api/sync.d.ts +3 -0
- package/dist-lib/lib/api/sync.js +6 -0
- package/dist-lib/lib/api/tool-activity.d.ts +5 -0
- package/dist-lib/lib/api/tool-activity.js +53 -0
- package/dist-lib/lib/api/tool-configs.d.ts +6 -0
- package/dist-lib/lib/api/tool-configs.js +27 -0
- package/dist-lib/lib/api/watchable.d.ts +12 -0
- package/dist-lib/lib/api/watchable.js +121 -0
- package/dist-lib/lib/assistant-client.d.ts +21 -0
- package/dist-lib/lib/assistant-client.js +52 -0
- package/dist-lib/lib/assistant-tree.d.ts +50 -0
- package/dist-lib/lib/assistant-tree.js +184 -0
- package/dist-lib/lib/assistant.d.ts +425 -0
- package/dist-lib/lib/assistant.js +2706 -0
- package/dist-lib/lib/audio-format.d.ts +4 -0
- package/dist-lib/lib/audio-format.js +4 -0
- package/dist-lib/lib/chat-title.d.ts +5 -0
- package/dist-lib/lib/chat-title.js +58 -0
- package/dist-lib/lib/client.d.ts +25 -0
- package/dist-lib/lib/client.js +53 -0
- package/dist-lib/lib/collabora/convert.d.ts +9 -0
- package/dist-lib/lib/collabora/convert.js +62 -0
- package/dist-lib/lib/collabora/entry.d.ts +1 -0
- package/dist-lib/lib/collabora/entry.js +44 -0
- package/dist-lib/lib/collabora/protocol.d.ts +15 -0
- package/dist-lib/lib/collabora/protocol.js +24 -0
- package/dist-lib/lib/collabora/worker.d.ts +5 -0
- package/dist-lib/lib/collabora/worker.js +111 -0
- package/dist-lib/lib/compaction.d.ts +30 -0
- package/dist-lib/lib/compaction.js +95 -0
- package/dist-lib/lib/constants.d.ts +4 -0
- package/dist-lib/lib/constants.js +4 -0
- package/dist-lib/lib/core/agent.d.ts +15 -0
- package/dist-lib/lib/core/agent.js +149 -0
- package/dist-lib/lib/core/app-build.d.ts +7 -0
- package/dist-lib/lib/core/app-build.js +1 -0
- package/dist-lib/lib/core/approval-rules.d.ts +15 -0
- package/dist-lib/lib/core/approval-rules.js +41 -0
- package/dist-lib/lib/core/async-iterable.d.ts +13 -0
- package/dist-lib/lib/core/async-iterable.js +65 -0
- package/dist-lib/lib/core/attachment-link.d.ts +7 -0
- package/dist-lib/lib/core/attachment-link.js +6 -0
- package/dist-lib/lib/core/audio-attachment.d.ts +1 -0
- package/dist-lib/lib/core/audio-attachment.js +4 -0
- package/dist-lib/lib/core/blind-peers.d.ts +6 -0
- package/dist-lib/lib/core/blind-peers.js +13 -0
- package/dist-lib/lib/core/chat-activity.d.ts +7 -0
- package/dist-lib/lib/core/chat-activity.js +244 -0
- package/dist-lib/lib/core/chat-group.d.ts +14 -0
- package/dist-lib/lib/core/chat-group.js +79 -0
- package/dist-lib/lib/core/chat-search.d.ts +17 -0
- package/dist-lib/lib/core/chat-search.js +199 -0
- package/dist-lib/lib/core/chat.d.ts +37 -0
- package/dist-lib/lib/core/chat.js +196 -0
- package/dist-lib/lib/core/chunk.d.ts +61 -0
- package/dist-lib/lib/core/chunk.js +212 -0
- package/dist-lib/lib/core/contract/activity.d.ts +11 -0
- package/dist-lib/lib/core/contract/activity.js +45 -0
- package/dist-lib/lib/core/contract/agents.d.ts +2 -0
- package/dist-lib/lib/core/contract/agents.js +29 -0
- package/dist-lib/lib/core/contract/approval-rules.d.ts +2 -0
- package/dist-lib/lib/core/contract/approval-rules.js +14 -0
- package/dist-lib/lib/core/contract/attestations.d.ts +2 -0
- package/dist-lib/lib/core/contract/attestations.js +14 -0
- package/dist-lib/lib/core/contract/chat-group.d.ts +2 -0
- package/dist-lib/lib/core/contract/chat-group.js +20 -0
- package/dist-lib/lib/core/contract/chat.d.ts +2 -0
- package/dist-lib/lib/core/contract/chat.js +18 -0
- package/dist-lib/lib/core/contract/chunks.d.ts +2 -0
- package/dist-lib/lib/core/contract/chunks.js +156 -0
- package/dist-lib/lib/core/contract/devices.d.ts +2 -0
- package/dist-lib/lib/core/contract/devices.js +110 -0
- package/dist-lib/lib/core/contract/index.d.ts +2 -0
- package/dist-lib/lib/core/contract/index.js +30 -0
- package/dist-lib/lib/core/contract/indexing.d.ts +4 -0
- package/dist-lib/lib/core/contract/indexing.js +82 -0
- package/dist-lib/lib/core/contract/knowledge.d.ts +2 -0
- package/dist-lib/lib/core/contract/knowledge.js +55 -0
- package/dist-lib/lib/core/contract/members.d.ts +2 -0
- package/dist-lib/lib/core/contract/members.js +96 -0
- package/dist-lib/lib/core/contract/seed-defaults.d.ts +2 -0
- package/dist-lib/lib/core/contract/seed-defaults.js +29 -0
- package/dist-lib/lib/core/contract/tool-activity.d.ts +2 -0
- package/dist-lib/lib/core/contract/tool-activity.js +25 -0
- package/dist-lib/lib/core/contract/tool-configs.d.ts +2 -0
- package/dist-lib/lib/core/contract/tool-configs.js +50 -0
- package/dist-lib/lib/core/contract/types.d.ts +11 -0
- package/dist-lib/lib/core/contract/types.js +1 -0
- package/dist-lib/lib/core/contract/writers.d.ts +2 -0
- package/dist-lib/lib/core/contract/writers.js +112 -0
- package/dist-lib/lib/core/cpu-info-node.d.ts +3 -0
- package/dist-lib/lib/core/cpu-info-node.js +2 -0
- package/dist-lib/lib/core/departures.d.ts +41 -0
- package/dist-lib/lib/core/departures.js +145 -0
- package/dist-lib/lib/core/device-attestation.d.ts +6 -0
- package/dist-lib/lib/core/device-attestation.js +12 -0
- package/dist-lib/lib/core/devices.d.ts +14 -0
- package/dist-lib/lib/core/devices.js +11 -0
- package/dist-lib/lib/core/document-attachment.d.ts +22 -0
- package/dist-lib/lib/core/document-attachment.js +35 -0
- package/dist-lib/lib/core/files/attachments.d.ts +8 -0
- package/dist-lib/lib/core/files/attachments.js +50 -0
- package/dist-lib/lib/core/files/index.d.ts +133 -0
- package/dist-lib/lib/core/files/index.js +585 -0
- package/dist-lib/lib/core/files/knowledge.d.ts +9 -0
- package/dist-lib/lib/core/files/knowledge.js +31 -0
- package/dist-lib/lib/core/files/preview/image.d.ts +4 -0
- package/dist-lib/lib/core/files/preview/image.js +42 -0
- package/dist-lib/lib/core/files/preview/index.d.ts +33 -0
- package/dist-lib/lib/core/files/preview/index.js +161 -0
- package/dist-lib/lib/core/files/processing/claims.d.ts +26 -0
- package/dist-lib/lib/core/files/processing/claims.js +89 -0
- package/dist-lib/lib/core/files/processing/document.d.ts +100 -0
- package/dist-lib/lib/core/files/processing/document.js +101 -0
- package/dist-lib/lib/core/files/processing/index.d.ts +75 -0
- package/dist-lib/lib/core/files/processing/index.js +295 -0
- package/dist-lib/lib/core/files/processing/ownership.d.ts +11 -0
- package/dist-lib/lib/core/files/processing/ownership.js +41 -0
- package/dist-lib/lib/core/files/processing/protocol.d.ts +29 -0
- package/dist-lib/lib/core/files/processing/protocol.js +70 -0
- package/dist-lib/lib/core/files/processing/settle.d.ts +10 -0
- package/dist-lib/lib/core/files/processing/settle.js +75 -0
- package/dist-lib/lib/core/files/processing/source.d.ts +36 -0
- package/dist-lib/lib/core/files/processing/source.js +96 -0
- package/dist-lib/lib/core/files/processing/state.d.ts +64 -0
- package/dist-lib/lib/core/files/processing/state.js +118 -0
- package/dist-lib/lib/core/files/rag/artifact/codec.d.ts +3 -0
- package/dist-lib/lib/core/files/rag/artifact/codec.js +172 -0
- package/dist-lib/lib/core/files/rag/artifact/id.d.ts +10 -0
- package/dist-lib/lib/core/files/rag/artifact/id.js +39 -0
- package/dist-lib/lib/core/files/rag/artifact/types.d.ts +24 -0
- package/dist-lib/lib/core/files/rag/artifact/types.js +41 -0
- package/dist-lib/lib/core/files/rag/build.d.ts +25 -0
- package/dist-lib/lib/core/files/rag/build.js +60 -0
- package/dist-lib/lib/core/files/rag/chunk/chunking.d.ts +12 -0
- package/dist-lib/lib/core/files/rag/chunk/chunking.js +117 -0
- package/dist-lib/lib/core/files/rag/chunk/metadata.d.ts +30 -0
- package/dist-lib/lib/core/files/rag/chunk/metadata.js +89 -0
- package/dist-lib/lib/core/files/rag/chunk/tokenizer.d.ts +7 -0
- package/dist-lib/lib/core/files/rag/chunk/tokenizer.js +18 -0
- package/dist-lib/lib/core/files/rag/document-types.d.ts +11 -0
- package/dist-lib/lib/core/files/rag/document-types.js +94 -0
- package/dist-lib/lib/core/files/rag/extract/csv-text.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/extract/csv-text.js +117 -0
- package/dist-lib/lib/core/files/rag/extract/document-types.d.ts +22 -0
- package/dist-lib/lib/core/files/rag/extract/document-types.js +152 -0
- package/dist-lib/lib/core/files/rag/extract/extract.d.ts +17 -0
- package/dist-lib/lib/core/files/rag/extract/extract.js +81 -0
- package/dist-lib/lib/core/files/rag/extract/html-table-text.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/extract/html-table-text.js +125 -0
- package/dist-lib/lib/core/files/rag/extract/html-text.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/extract/html-text.js +132 -0
- package/dist-lib/lib/core/files/rag/extract/office-collabora.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/extract/office-collabora.js +8 -0
- package/dist-lib/lib/core/files/rag/extract/pdf-platform.d.ts +17 -0
- package/dist-lib/lib/core/files/rag/extract/pdf-platform.js +141 -0
- package/dist-lib/lib/core/files/rag/extract/pdf-text.d.ts +12 -0
- package/dist-lib/lib/core/files/rag/extract/pdf-text.js +43 -0
- package/dist-lib/lib/core/files/rag/extract/rtf-text.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/extract/rtf-text.js +15 -0
- package/dist-lib/lib/core/files/rag/extract/text-decoding.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/extract/text-decoding.js +91 -0
- package/dist-lib/lib/core/files/rag/extract/types.d.ts +36 -0
- package/dist-lib/lib/core/files/rag/extract/types.js +12 -0
- package/dist-lib/lib/core/files/rag/extract/utf.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/extract/utf.js +18 -0
- package/dist-lib/lib/core/files/rag/index.d.ts +54 -0
- package/dist-lib/lib/core/files/rag/index.js +79 -0
- package/dist-lib/lib/core/files/rag/limits.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/limits.js +2 -0
- package/dist-lib/lib/core/files/rag/load.d.ts +28 -0
- package/dist-lib/lib/core/files/rag/load.js +250 -0
- package/dist-lib/lib/core/files/rag/models.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/models.js +1 -0
- package/dist-lib/lib/core/files/rag/policy.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/policy.js +23 -0
- package/dist-lib/lib/core/files/rag/produce.d.ts +21 -0
- package/dist-lib/lib/core/files/rag/produce.js +214 -0
- package/dist-lib/lib/core/files/rag/retrieval/citation-match.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/retrieval/citation-match.js +16 -0
- package/dist-lib/lib/core/files/rag/retrieval/coverage.d.ts +4 -0
- package/dist-lib/lib/core/files/rag/retrieval/coverage.js +48 -0
- package/dist-lib/lib/core/files/rag/retrieval/diversity.d.ts +10 -0
- package/dist-lib/lib/core/files/rag/retrieval/diversity.js +37 -0
- package/dist-lib/lib/core/files/rag/retrieval/floor.d.ts +5 -0
- package/dist-lib/lib/core/files/rag/retrieval/floor.js +21 -0
- package/dist-lib/lib/core/files/rag/retrieval/format.d.ts +6 -0
- package/dist-lib/lib/core/files/rag/retrieval/format.js +21 -0
- package/dist-lib/lib/core/files/rag/retrieval/full-text.d.ts +22 -0
- package/dist-lib/lib/core/files/rag/retrieval/full-text.js +137 -0
- package/dist-lib/lib/core/files/rag/retrieval/knowledge.d.ts +2 -0
- package/dist-lib/lib/core/files/rag/retrieval/knowledge.js +53 -0
- package/dist-lib/lib/core/files/rag/retrieval/notices.d.ts +16 -0
- package/dist-lib/lib/core/files/rag/retrieval/notices.js +27 -0
- package/dist-lib/lib/core/files/rag/retrieval/scoring.d.ts +8 -0
- package/dist-lib/lib/core/files/rag/retrieval/scoring.js +82 -0
- package/dist-lib/lib/core/files/rag/retrieval/search.d.ts +3 -0
- package/dist-lib/lib/core/files/rag/retrieval/search.js +46 -0
- package/dist-lib/lib/core/files/rag/retrieval/selection.d.ts +10 -0
- package/dist-lib/lib/core/files/rag/retrieval/selection.js +63 -0
- package/dist-lib/lib/core/files/rag/retrieval/source-tag-filter.d.ts +6 -0
- package/dist-lib/lib/core/files/rag/retrieval/source-tag-filter.js +79 -0
- package/dist-lib/lib/core/files/rag/retrieval/sources.d.ts +13 -0
- package/dist-lib/lib/core/files/rag/retrieval/sources.js +20 -0
- package/dist-lib/lib/core/files/rag/retrieval/target.d.ts +13 -0
- package/dist-lib/lib/core/files/rag/retrieval/target.js +89 -0
- package/dist-lib/lib/core/files/rag/retrieval/terms.d.ts +8 -0
- package/dist-lib/lib/core/files/rag/retrieval/terms.js +70 -0
- package/dist-lib/lib/core/files/rag/retrieval/types.d.ts +28 -0
- package/dist-lib/lib/core/files/rag/retrieval/types.js +1 -0
- package/dist-lib/lib/core/files/rag/retrieval/web-sources.d.ts +8 -0
- package/dist-lib/lib/core/files/rag/retrieval/web-sources.js +41 -0
- package/dist-lib/lib/core/files/rag/retrieval/whole-document-query.d.ts +1 -0
- package/dist-lib/lib/core/files/rag/retrieval/whole-document-query.js +96 -0
- package/dist-lib/lib/core/files/rag/vector/memory.d.ts +15 -0
- package/dist-lib/lib/core/files/rag/vector/memory.js +135 -0
- package/dist-lib/lib/core/files/rag/vector/types.d.ts +28 -0
- package/dist-lib/lib/core/files/rag/vector/types.js +1 -0
- package/dist-lib/lib/core/files/records/chunks.d.ts +3 -0
- package/dist-lib/lib/core/files/records/chunks.js +12 -0
- package/dist-lib/lib/core/files/records/index.d.ts +10 -0
- package/dist-lib/lib/core/files/records/index.js +8 -0
- package/dist-lib/lib/core/files/records/knowledge.d.ts +22 -0
- package/dist-lib/lib/core/files/records/knowledge.js +105 -0
- package/dist-lib/lib/core/files/records/linkage.d.ts +11 -0
- package/dist-lib/lib/core/files/records/linkage.js +27 -0
- package/dist-lib/lib/core/files/records/output.d.ts +8 -0
- package/dist-lib/lib/core/files/records/output.js +38 -0
- package/dist-lib/lib/core/files/records/reclaim.d.ts +5 -0
- package/dist-lib/lib/core/files/records/reclaim.js +73 -0
- package/dist-lib/lib/core/files/records/routing.d.ts +23 -0
- package/dist-lib/lib/core/files/records/routing.js +62 -0
- package/dist-lib/lib/core/files/records/source.d.ts +16 -0
- package/dist-lib/lib/core/files/records/source.js +70 -0
- package/dist-lib/lib/core/files/records/sweep.d.ts +2 -0
- package/dist-lib/lib/core/files/records/sweep.js +30 -0
- package/dist-lib/lib/core/files/store/blob.d.ts +29 -0
- package/dist-lib/lib/core/files/store/blob.js +98 -0
- package/dist-lib/lib/core/files/store/index.d.ts +36 -0
- package/dist-lib/lib/core/files/store/index.js +89 -0
- package/dist-lib/lib/core/files/store/inline.d.ts +8 -0
- package/dist-lib/lib/core/files/store/inline.js +38 -0
- package/dist-lib/lib/core/files/store/reference.d.ts +27 -0
- package/dist-lib/lib/core/files/store/reference.js +168 -0
- package/dist-lib/lib/core/files/store/server.d.ts +27 -0
- package/dist-lib/lib/core/files/store/server.js +114 -0
- package/dist-lib/lib/core/files/store/sweep.d.ts +2 -0
- package/dist-lib/lib/core/files/store/sweep.js +45 -0
- package/dist-lib/lib/core/files/upload-input.d.ts +27 -0
- package/dist-lib/lib/core/files/upload-input.js +52 -0
- package/dist-lib/lib/core/files/upload.d.ts +32 -0
- package/dist-lib/lib/core/files/upload.js +201 -0
- package/dist-lib/lib/core/fs-drives.d.ts +18 -0
- package/dist-lib/lib/core/fs-drives.js +58 -0
- package/dist-lib/lib/core/gpu-info-node.d.ts +3 -0
- package/dist-lib/lib/core/gpu-info-node.js +2 -0
- package/dist-lib/lib/core/handoff-channel.d.ts +46 -0
- package/dist-lib/lib/core/handoff-channel.js +102 -0
- package/dist-lib/lib/core/health-assistant.d.ts +7 -0
- package/dist-lib/lib/core/health-assistant.js +24 -0
- package/dist-lib/lib/core/helpers/index.d.ts +5 -0
- package/dist-lib/lib/core/helpers/index.js +74 -0
- package/dist-lib/lib/core/hosted-agents.d.ts +36 -0
- package/dist-lib/lib/core/hosted-agents.js +127 -0
- package/dist-lib/lib/core/id.d.ts +1 -0
- package/dist-lib/lib/core/id.js +5 -0
- package/dist-lib/lib/core/identity.d.ts +14 -0
- package/dist-lib/lib/core/identity.js +45 -0
- package/dist-lib/lib/core/image/cache-sweep.d.ts +12 -0
- package/dist-lib/lib/core/image/cache-sweep.js +67 -0
- package/dist-lib/lib/core/image/normalize.d.ts +3 -0
- package/dist-lib/lib/core/image/normalize.js +49 -0
- package/dist-lib/lib/core/image/sniff.d.ts +3 -0
- package/dist-lib/lib/core/image/sniff.js +149 -0
- package/dist-lib/lib/core/invite.d.ts +14 -0
- package/dist-lib/lib/core/invite.js +36 -0
- package/dist-lib/lib/core/local/index.d.ts +71 -0
- package/dist-lib/lib/core/local/index.js +391 -0
- package/dist-lib/lib/core/membership.d.ts +15 -0
- package/dist-lib/lib/core/membership.js +40 -0
- package/dist-lib/lib/core/mesh.d.ts +153 -0
- package/dist-lib/lib/core/mesh.js +509 -0
- package/dist-lib/lib/core/migrations/agent-catalog.d.ts +6 -0
- package/dist-lib/lib/core/migrations/agent-catalog.js +17 -0
- package/dist-lib/lib/core/migrations/index.d.ts +5 -0
- package/dist-lib/lib/core/migrations/index.js +7 -0
- package/dist-lib/lib/core/migrations/membership.d.ts +11 -0
- package/dist-lib/lib/core/migrations/membership.js +26 -0
- package/dist-lib/lib/core/min-version.d.ts +6 -0
- package/dist-lib/lib/core/min-version.js +31 -0
- package/dist-lib/lib/core/next-frame.d.ts +6 -0
- package/dist-lib/lib/core/next-frame.js +27 -0
- package/dist-lib/lib/core/notification.d.ts +6 -0
- package/dist-lib/lib/core/notification.js +162 -0
- package/dist-lib/lib/core/ota-config.d.ts +19 -0
- package/dist-lib/lib/core/ota-config.js +44 -0
- package/dist-lib/lib/core/prompt-history.d.ts +12 -0
- package/dist-lib/lib/core/prompt-history.js +40 -0
- package/dist-lib/lib/core/search-fold.d.ts +1 -0
- package/dist-lib/lib/core/search-fold.js +4 -0
- package/dist-lib/lib/core/secret-handoff.d.ts +17 -0
- package/dist-lib/lib/core/secret-handoff.js +29 -0
- package/dist-lib/lib/core/send.d.ts +18 -0
- package/dist-lib/lib/core/send.js +46 -0
- package/dist-lib/lib/core/skill-overlay-handoff.d.ts +25 -0
- package/dist-lib/lib/core/skill-overlay-handoff.js +50 -0
- package/dist-lib/lib/core/skill-overlay-receiver.d.ts +8 -0
- package/dist-lib/lib/core/skill-overlay-receiver.js +20 -0
- package/dist-lib/lib/core/skill-secrets.d.ts +25 -0
- package/dist-lib/lib/core/skill-secrets.js +79 -0
- package/dist-lib/lib/core/sync/chats.d.ts +3 -0
- package/dist-lib/lib/core/sync/chats.js +5 -0
- package/dist-lib/lib/core/sync/chunks.d.ts +9 -0
- package/dist-lib/lib/core/sync/chunks.js +16 -0
- package/dist-lib/lib/core/sync/devices.d.ts +4 -0
- package/dist-lib/lib/core/sync/devices.js +12 -0
- package/dist-lib/lib/core/sync/files.d.ts +16 -0
- package/dist-lib/lib/core/sync/files.js +109 -0
- package/dist-lib/lib/core/sync/index.d.ts +5 -0
- package/dist-lib/lib/core/sync/index.js +63 -0
- package/dist-lib/lib/core/sync-status.d.ts +17 -0
- package/dist-lib/lib/core/sync-status.js +129 -0
- package/dist-lib/lib/core/system-info.d.ts +24 -0
- package/dist-lib/lib/core/system-info.js +66 -0
- package/dist-lib/lib/core/tool-activity.d.ts +22 -0
- package/dist-lib/lib/core/tool-activity.js +39 -0
- package/dist-lib/lib/core/tool-configs.d.ts +13 -0
- package/dist-lib/lib/core/tool-configs.js +141 -0
- package/dist-lib/lib/core.d.ts +291 -0
- package/dist-lib/lib/core.js +2126 -0
- package/dist-lib/lib/credential-pusher.d.ts +67 -0
- package/dist-lib/lib/credential-pusher.js +290 -0
- package/dist-lib/lib/custom-model-pusher.d.ts +9 -0
- package/dist-lib/lib/custom-model-pusher.js +42 -0
- package/dist-lib/lib/custom-model-source.d.ts +15 -0
- package/dist-lib/lib/custom-model-source.js +46 -0
- package/dist-lib/lib/deadline.d.ts +9 -0
- package/dist-lib/lib/deadline.js +31 -0
- package/dist-lib/lib/diagnostics.d.ts +35 -0
- package/dist-lib/lib/diagnostics.js +14 -0
- package/dist-lib/lib/fetch-node.d.ts +1 -0
- package/dist-lib/lib/fetch-node.js +1 -0
- package/dist-lib/lib/fs-drive-bridge.d.ts +12 -0
- package/dist-lib/lib/fs-drive-bridge.js +186 -0
- package/dist-lib/lib/fs-grant-keys.d.ts +2 -0
- package/dist-lib/lib/fs-grant-keys.js +6 -0
- package/dist-lib/lib/fs-grants.d.ts +10 -0
- package/dist-lib/lib/fs-grants.js +44 -0
- package/dist-lib/lib/image-cache-dir.d.ts +1 -0
- package/dist-lib/lib/image-cache-dir.js +5 -0
- package/dist-lib/lib/integrations/index.d.ts +4 -0
- package/dist-lib/lib/integrations/index.js +3 -0
- package/dist-lib/lib/integrations/mcp-providers.d.ts +12 -0
- package/dist-lib/lib/integrations/mcp-providers.js +22 -0
- package/dist-lib/lib/integrations/oauth-providers.d.ts +14 -0
- package/dist-lib/lib/integrations/oauth-providers.js +116 -0
- package/dist-lib/lib/integrations/setup-routes.d.ts +12 -0
- package/dist-lib/lib/integrations/setup-routes.js +114 -0
- package/dist-lib/lib/knowledge-search.d.ts +7 -0
- package/dist-lib/lib/knowledge-search.js +53 -0
- package/dist-lib/lib/kv-cache-reclaim.d.ts +5 -0
- package/dist-lib/lib/kv-cache-reclaim.js +29 -0
- package/dist-lib/lib/kv-cache.d.ts +18 -0
- package/dist-lib/lib/kv-cache.js +33 -0
- package/dist-lib/lib/legacy-core-domain.d.ts +258 -0
- package/dist-lib/lib/legacy-core-domain.js +1 -0
- package/dist-lib/lib/llm-model-names.d.ts +1 -0
- package/dist-lib/lib/llm-model-names.js +1 -0
- package/dist-lib/lib/log-buffer.d.ts +7 -0
- package/dist-lib/lib/log-buffer.js +16 -0
- package/dist-lib/lib/log-levels.d.ts +7 -0
- package/dist-lib/lib/log-levels.js +1 -0
- package/dist-lib/lib/log.d.ts +60 -0
- package/dist-lib/lib/log.js +149 -0
- package/dist-lib/lib/logger-type.d.ts +13 -0
- package/dist-lib/lib/logger-type.js +1 -0
- package/dist-lib/lib/logger.d.ts +5 -0
- package/dist-lib/lib/logger.js +18 -0
- package/dist-lib/lib/model-catalog-revision.d.ts +9 -0
- package/dist-lib/lib/model-catalog-revision.js +65 -0
- package/dist-lib/lib/model-clamp.d.ts +6 -0
- package/dist-lib/lib/model-clamp.js +20 -0
- package/dist-lib/lib/model-context.d.ts +13 -0
- package/dist-lib/lib/model-context.js +71 -0
- package/dist-lib/lib/model-download-status.d.ts +29 -0
- package/dist-lib/lib/model-download-status.js +56 -0
- package/dist-lib/lib/model-download.d.ts +191 -0
- package/dist-lib/lib/model-download.js +787 -0
- package/dist-lib/lib/oauth/asana.d.ts +35 -0
- package/dist-lib/lib/oauth/asana.js +71 -0
- package/dist-lib/lib/oauth/browser-connect.d.ts +22 -0
- package/dist-lib/lib/oauth/browser-connect.js +83 -0
- package/dist-lib/lib/oauth/client.d.ts +9 -0
- package/dist-lib/lib/oauth/client.js +9 -0
- package/dist-lib/lib/oauth/common.d.ts +38 -0
- package/dist-lib/lib/oauth/common.js +36 -0
- package/dist-lib/lib/oauth/crypto.d.ts +3 -0
- package/dist-lib/lib/oauth/crypto.js +25 -0
- package/dist-lib/lib/oauth/fetch-platform.d.ts +1 -0
- package/dist-lib/lib/oauth/fetch-platform.js +1 -0
- package/dist-lib/lib/oauth/flow-core.d.ts +6 -0
- package/dist-lib/lib/oauth/flow-core.js +37 -0
- package/dist-lib/lib/oauth/flow.d.ts +3 -0
- package/dist-lib/lib/oauth/flow.js +24 -0
- package/dist-lib/lib/oauth/google.d.ts +37 -0
- package/dist-lib/lib/oauth/google.js +77 -0
- package/dist-lib/lib/oauth/host.d.ts +5 -0
- package/dist-lib/lib/oauth/host.js +3 -0
- package/dist-lib/lib/oauth/keys.d.ts +25 -0
- package/dist-lib/lib/oauth/keys.js +93 -0
- package/dist-lib/lib/oauth/loopback.d.ts +3 -0
- package/dist-lib/lib/oauth/loopback.js +119 -0
- package/dist-lib/lib/oauth/loopback.stub.d.ts +3 -0
- package/dist-lib/lib/oauth/loopback.stub.js +6 -0
- package/dist-lib/lib/oauth/notion.d.ts +34 -0
- package/dist-lib/lib/oauth/notion.js +82 -0
- package/dist-lib/lib/oauth/pkce-runtime.d.ts +3 -0
- package/dist-lib/lib/oauth/pkce-runtime.js +5 -0
- package/dist-lib/lib/oauth/pkce.d.ts +8 -0
- package/dist-lib/lib/oauth/pkce.js +6 -0
- package/dist-lib/lib/oauth/providers.d.ts +5 -0
- package/dist-lib/lib/oauth/providers.js +73 -0
- package/dist-lib/lib/oauth/redirect-types.d.ts +19 -0
- package/dist-lib/lib/oauth/redirect-types.js +1 -0
- package/dist-lib/lib/oauth/register.d.ts +3 -0
- package/dist-lib/lib/oauth/register.js +34 -0
- package/dist-lib/lib/oauth/resolve.d.ts +17 -0
- package/dist-lib/lib/oauth/resolve.js +57 -0
- package/dist-lib/lib/oauth/spotify.d.ts +32 -0
- package/dist-lib/lib/oauth/spotify.js +73 -0
- package/dist-lib/lib/oauth/token-exchange-core.d.ts +6 -0
- package/dist-lib/lib/oauth/token-exchange-core.js +97 -0
- package/dist-lib/lib/oauth/token-exchange.d.ts +5 -0
- package/dist-lib/lib/oauth/token-exchange.js +9 -0
- package/dist-lib/lib/oauth/types.d.ts +54 -0
- package/dist-lib/lib/oauth/types.js +1 -0
- package/dist-lib/lib/oauth.client.d.ts +5 -0
- package/dist-lib/lib/oauth.client.js +2 -0
- package/dist-lib/lib/oauth.d.ts +14 -0
- package/dist-lib/lib/oauth.js +12 -0
- package/dist-lib/lib/obsidian-key.d.ts +1 -0
- package/dist-lib/lib/obsidian-key.js +1 -0
- package/dist-lib/lib/race-resource.d.ts +5 -0
- package/dist-lib/lib/race-resource.js +25 -0
- package/dist-lib/lib/reconnecting-watch.d.ts +2 -0
- package/dist-lib/lib/reconnecting-watch.js +78 -0
- package/dist-lib/lib/retry-budget.d.ts +14 -0
- package/dist-lib/lib/retry-budget.js +39 -0
- package/dist-lib/lib/run-notices.d.ts +23 -0
- package/dist-lib/lib/run-notices.js +33 -0
- package/dist-lib/lib/run-transcript.d.ts +35 -0
- package/dist-lib/lib/run-transcript.js +106 -0
- package/dist-lib/lib/skill-readiness.d.ts +2 -0
- package/dist-lib/lib/skill-readiness.js +21 -0
- package/dist-lib/lib/skill-types.d.ts +70 -0
- package/dist-lib/lib/skill-types.js +1 -0
- package/dist-lib/lib/spec-loader.d.ts +14 -0
- package/dist-lib/lib/spec-loader.js +9 -0
- package/dist-lib/lib/suspend-lifecycle.d.ts +17 -0
- package/dist-lib/lib/suspend-lifecycle.js +33 -0
- package/dist-lib/lib/task.d.ts +26 -0
- package/dist-lib/lib/task.js +69 -0
- package/dist-lib/lib/unref.d.ts +3 -0
- package/dist-lib/lib/unref.js +4 -0
- package/dist-lib/lib/wav-duration.d.ts +1 -0
- package/dist-lib/lib/wav-duration.js +53 -0
- package/dist-lib/lib/write-queue.d.ts +22 -0
- package/dist-lib/lib/write-queue.js +39 -0
- package/dist-lib/schema/build.d.ts +1 -0
- package/dist-lib/schema/build.js +120 -0
- package/dist-lib/schema/config.d.ts +4 -0
- package/dist-lib/schema/config.js +7 -0
- package/dist-lib/schema/content.d.ts +20 -0
- package/dist-lib/schema/content.js +122 -0
- package/dist-lib/schema/engine.d.ts +2 -0
- package/dist-lib/schema/engine.js +2013 -0
- package/dist-lib/schema/field-order.d.ts +1 -0
- package/dist-lib/schema/field-order.js +95 -0
- package/dist-lib/schema/local.d.ts +2 -0
- package/dist-lib/schema/local.js +133 -0
- package/dist-lib/schema/qvac.d.ts +22 -0
- package/dist-lib/schema/qvac.js +722 -0
- package/dist-lib/spec/bind.d.ts +2 -0
- package/dist-lib/spec/bind.js +520 -0
- package/dist-lib/spec/calls.d.ts +3 -0
- package/dist-lib/spec/calls.js +129 -0
- package/dist-lib/spec/capabilities.d.ts +263 -0
- package/dist-lib/spec/config/hyperconf/index.d.ts +15 -0
- package/dist-lib/spec/config/hyperconf/index.js +24 -0
- package/dist-lib/spec/config/hyperconf/package.json +3 -0
- package/dist-lib/spec/config/hyperschema/index.js +81 -0
- package/dist-lib/spec/config/hyperschema/schema.json +19 -0
- package/dist-lib/spec/hrpc/hrpc.json +1500 -0
- package/dist-lib/spec/hrpc/index.d.ts +3 -0
- package/dist-lib/spec/hrpc/index.js +1545 -0
- package/dist-lib/spec/hrpc/messages.d.ts +4 -0
- package/dist-lib/spec/hrpc/messages.js +8373 -0
- package/dist-lib/spec/hyperschema/index.d.ts +20 -0
- package/dist-lib/spec/hyperschema/index.js +8373 -0
- package/dist-lib/spec/hyperschema/schema.json +6202 -0
- package/dist-lib/spec/hyperschema/types.d.ts +1545 -0
- package/dist-lib/spec/index.d.ts +36 -0
- package/dist-lib/spec/index.js +6 -0
- package/dist-lib/spec/local/hyperdb/db.json +186 -0
- package/dist-lib/spec/local/hyperdb/index.d.ts +9 -0
- package/dist-lib/spec/local/hyperdb/index.js +855 -0
- package/dist-lib/spec/local/hyperdb/messages.js +2189 -0
- package/dist-lib/spec/local/hyperschema/index.js +1208 -0
- package/dist-lib/spec/local/hyperschema/schema.json +639 -0
- package/dist-lib/spec/local/hyperschema/types.d.ts +159 -0
- package/dist-lib/spec/mesh/hyperdb/db.json +702 -0
- package/dist-lib/spec/mesh/hyperdb/index.d.ts +9 -0
- package/dist-lib/spec/mesh/hyperdb/index.js +2260 -0
- package/dist-lib/spec/mesh/hyperdb/messages.js +7453 -0
- package/dist-lib/spec/mesh/hyperdispatch/dispatch.json +174 -0
- package/dist-lib/spec/mesh/hyperdispatch/index.d.ts +13 -0
- package/dist-lib/spec/mesh/hyperdispatch/index.js +566 -0
- package/dist-lib/spec/mesh/hyperdispatch/messages.js +5713 -0
- package/dist-lib/spec/mesh/hyperschema/index.d.ts +14 -0
- package/dist-lib/spec/mesh/hyperschema/index.js +5713 -0
- package/dist-lib/spec/mesh/hyperschema/schema.json +3142 -0
- package/dist-lib/spec/mesh/hyperschema/types.d.ts +754 -0
- package/ducks.png +0 -0
- package/package.json +332 -1
|
@@ -0,0 +1,2706 @@
|
|
|
1
|
+
import defaultFetch from '#fetch';
|
|
2
|
+
import b4a from 'b4a';
|
|
3
|
+
import { AbortController } from 'bare-abort-controller';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import crypto from 'hypercore-crypto';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { attachmentTurn, skillLoadedTurn } from '@qvac/harness/lib/capabilities/tool-turns.ts';
|
|
9
|
+
import { diffusionDownloadName, mediaModelNameFor } from '@qvac/harness/lib/models/model-catalog.ts';
|
|
10
|
+
import { readSetup as normalizeSkillSetup } from '@qvac/harness/lib/tools/skills/manifest-parse.ts';
|
|
11
|
+
import { SKILL_STATE } from '@qvac/harness/lib/tools/skills/skill-state.ts';
|
|
12
|
+
import { toolsetEnabled } from '@qvac/harness/lib/tools/toolsets.ts';
|
|
13
|
+
import { RAG_CITATION_LINE } from '@qvac/harness/lib/prompt.ts';
|
|
14
|
+
import { acceptedRepairCount, historyBranches, isPromptChunk } from "./core/prompt-history.js";
|
|
15
|
+
import ReadyGuard from 'ready-guard';
|
|
16
|
+
import ReadyResource from 'ready-resource';
|
|
17
|
+
import { local } from '../spec/index.js';
|
|
18
|
+
import { isMeshTransition } from "./api/current-mesh.js";
|
|
19
|
+
import { buildTitlePrompt, deriveChatTitle, isDefaultChatTitle, parseTitleResponse } from "./chat-title.js";
|
|
20
|
+
import { matchApprovalRule } from "./core/approval-rules.js";
|
|
21
|
+
import { attachTurnDocumentIds, groupAttachmentsByMessage, formatRagContext, chooseRagNotice, RAG_KNOWLEDGE_OFF_NOTICE, RAG_TRUNCATED_NOTICE, RAG_UNAVAILABLE_NOTICE, createRagSourceTagFilter, citedByPhraseOverlap, formatFullDocumentContext, isFullTextHit, isTruncatedFullTextHit, sourcesFromHits, WEB_TOOLS, webSourcesFromToolResult, withoutDataChannel } from "./core/files/index.js";
|
|
22
|
+
import { isAudioAttachment } from "./core/audio-attachment.js";
|
|
23
|
+
import { annotateDocumentAttachments, documentUnreadNotice, documentUnreadReason, isDocumentAttachment } from "./core/document-attachment.js";
|
|
24
|
+
import { HISTORY_CHUNK_TYPES } from "./core/chunk.js";
|
|
25
|
+
import { imageCacheName, sweepImageCache } from "./core/image/cache-sweep.js";
|
|
26
|
+
import { IMAGE_MIMES, normalizeImageForModel } from "./core/image/normalize.js";
|
|
27
|
+
import { summarizeRuns } from "./diagnostics.js";
|
|
28
|
+
import { resolveFsGrants } from "./fs-drive-bridge.js";
|
|
29
|
+
import { fsGrantKey, fsWriteGrantKey } from "./fs-grant-keys.js";
|
|
30
|
+
import { fsGrantsFrom, grantedDirsFrom } from "./fs-grants.js";
|
|
31
|
+
import { defaultImageCacheDir } from "./image-cache-dir.js";
|
|
32
|
+
import { ModelCatalogRevisionHub } from "./model-catalog-revision.js";
|
|
33
|
+
import { agentRekeyFingerprint, chatKvCacheKey, chatRepairKvCacheKey, disappearedChatIds } from "./kv-cache-reclaim.js";
|
|
34
|
+
import { buildCompactionPrompt, canCompact, foldReducesPrompt, COMPACTION_KEEP_TURNS, COMPACTION_TURN_PREFIX, isContextOverflowFailure, nearContextLimit, compactionBoundary, compactionTurn, foldCompactedChunks, lastCompactedThroughId, parseCompactionSummary } from "./compaction.js";
|
|
35
|
+
import { isMobilePlatform, runnableModelName } from "./model-clamp.js";
|
|
36
|
+
import { ModelDownloadReconciler } from "./model-download.js";
|
|
37
|
+
import { AbortedError, raceAbort } from '@qvac/harness/race-abort';
|
|
38
|
+
import { RunTranscript } from "./run-transcript.js";
|
|
39
|
+
import { searchKnowledge } from "./knowledge-search.js";
|
|
40
|
+
import { isLifecycleTeardown, knownAbortOutcome, RUN_ABORT_REASONS, RUN_FAILURE_REASONS, runFailureNotice } from "./run-notices.js";
|
|
41
|
+
import { encodeAgentSkillReadiness } from "./skill-readiness.js";
|
|
42
|
+
const QvacState = local.getEnum('@local/qvac-state');
|
|
43
|
+
export const IMAGE_ONLY_USER_PROMPT = 'The user sent one or more images without any accompanying text. Describe what the images show and respond to anything notable in them.';
|
|
44
|
+
export const DOCUMENT_ONLY_USER_PROMPT = 'The user sent one or more documents without any accompanying text. Summarize the attached documents.';
|
|
45
|
+
export const IMAGE_AND_DOCUMENT_USER_PROMPT = 'The user sent one or more images together with one or more documents, and no accompanying text. Describe what the images show and summarize the attached documents.';
|
|
46
|
+
function backfillDocumentOnlyTurn(messages) {
|
|
47
|
+
const last = messages.findLast((turn) => turn.role === 'user');
|
|
48
|
+
if (!last || last.content.trim())
|
|
49
|
+
return;
|
|
50
|
+
last.content = last.attachments?.length
|
|
51
|
+
? IMAGE_AND_DOCUMENT_USER_PROMPT
|
|
52
|
+
: DOCUMENT_ONLY_USER_PROMPT;
|
|
53
|
+
}
|
|
54
|
+
function backfillImageOnlyTurns(messages) {
|
|
55
|
+
for (const turn of messages) {
|
|
56
|
+
if (turn.role !== 'user' || turn.content.trim())
|
|
57
|
+
continue;
|
|
58
|
+
if (turn.attachments?.length)
|
|
59
|
+
turn.content = IMAGE_ONLY_USER_PROMPT;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function unreadableImagesNotice(fileNames) {
|
|
63
|
+
const plural = fileNames.length > 1;
|
|
64
|
+
return (`[Attached image${plural ? 's' : ''}: ${fileNames.join(', ')} — could not be read, so ` +
|
|
65
|
+
`${plural ? 'they are' : 'it is'} not visible to you. Tell the user the upload failed and ` +
|
|
66
|
+
`ask for a re-upload; never describe or recreate ${plural ? 'them' : 'it'}]`);
|
|
67
|
+
}
|
|
68
|
+
export function annotateUnreadableImages(unreadable) {
|
|
69
|
+
for (const [turn, names] of unreadable) {
|
|
70
|
+
const notice = unreadableImagesNotice(names);
|
|
71
|
+
turn.content = turn.content ? `${turn.content}\n${notice}` : notice;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function raceInactivity(promise, signal, timeoutMs) {
|
|
75
|
+
let timer = null;
|
|
76
|
+
const expired = new Promise((_, reject) => {
|
|
77
|
+
timer = setTimeout(() => reject(new Error('blob read timed out')), timeoutMs);
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
return await raceAbort(Promise.race([promise, expired]), signal);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (timer !== null)
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function drainBody(body, signal, timeoutMs) {
|
|
88
|
+
if (!body)
|
|
89
|
+
return null;
|
|
90
|
+
const reader = body.getReader();
|
|
91
|
+
const parts = [];
|
|
92
|
+
try {
|
|
93
|
+
for (;;) {
|
|
94
|
+
const read = await raceInactivity(reader.read(), signal, timeoutMs);
|
|
95
|
+
if (read.done)
|
|
96
|
+
break;
|
|
97
|
+
parts.push(b4a.from(read.value));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
await reader.cancel();
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return b4a.concat(parts);
|
|
105
|
+
}
|
|
106
|
+
export function unheardAudioNotice(label, reason, opts = {}) {
|
|
107
|
+
const retry = opts.retryable === false ? '' : ', and ask them to try again';
|
|
108
|
+
return (`${label} could not be transcribed (${reason}). Tell the user the audio was not ` +
|
|
109
|
+
`transcribed and why${retry}; never invent what it might contain.`);
|
|
110
|
+
}
|
|
111
|
+
function silentAudioNotice(fileName) {
|
|
112
|
+
return (`Attached audio "${fileName}" was transcribed and contained no detectable speech. ` +
|
|
113
|
+
'Tell the user; never invent what it might contain.');
|
|
114
|
+
}
|
|
115
|
+
function audioTranscriptBlock(fileName, transcript) {
|
|
116
|
+
return [
|
|
117
|
+
`The audio file "${fileName}" has already been transcribed on-device. You cannot open the file itself — use the transcript below as its full content and answer from it.`,
|
|
118
|
+
'Transcript:',
|
|
119
|
+
transcript
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
export function harnessListModels(harness) {
|
|
123
|
+
return async ({ type, cachedOnly, fullRegistry } = {}) => ({
|
|
124
|
+
models: [
|
|
125
|
+
...((await harness.list?.({
|
|
126
|
+
type: type ?? undefined,
|
|
127
|
+
cachedOnly: cachedOnly ?? undefined,
|
|
128
|
+
fullRegistry: fullRegistry ?? undefined
|
|
129
|
+
})) ?? [])
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function harnessDeleteModel(harness) {
|
|
134
|
+
return async ({ name }) => (await harness.deleteModel?.(name)) ?? { name, removed: 0, freedBytes: 0 };
|
|
135
|
+
}
|
|
136
|
+
export function harnessClearKvCache(harness) {
|
|
137
|
+
return async () => (await harness.clearKvCache?.()) ?? {
|
|
138
|
+
status: 'unsupported',
|
|
139
|
+
error: 'Harness does not support KV cache deletion'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function harnessTranscribe(harness) {
|
|
143
|
+
return async ({ audio, model }) => {
|
|
144
|
+
if (!harness.transcribe)
|
|
145
|
+
throw new Error('harness does not support transcribe');
|
|
146
|
+
return { text: await harness.transcribe({ audio, model: model ?? '' }) };
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function harnessSpeak(harness) {
|
|
150
|
+
return async ({ text, model }) => {
|
|
151
|
+
if (!harness.speak)
|
|
152
|
+
throw new Error('harness does not support speak');
|
|
153
|
+
const { audio, sampleRate } = await harness.speak({ text, model: model ?? '' });
|
|
154
|
+
return { audio, sampleRate };
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function harnessSkillCatalog(harness, syncCredentials) {
|
|
158
|
+
return async (agentId, modelName, options) => {
|
|
159
|
+
if (options?.refresh)
|
|
160
|
+
await syncCredentials?.();
|
|
161
|
+
return {
|
|
162
|
+
skills: ((await harness.skills?.(agentId, modelName, options)) ?? []).map(toEngineSkill)
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export function harnessSkillManagement(harness) {
|
|
167
|
+
return {
|
|
168
|
+
async listManagedSkills() {
|
|
169
|
+
return {
|
|
170
|
+
skills: ((await harness.listManagedSkills?.()) ?? []).map((skill) => ({
|
|
171
|
+
name: skill.name,
|
|
172
|
+
origin: skill.origin,
|
|
173
|
+
overridesBase: skill.overridesBase
|
|
174
|
+
}))
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
async readSkillFile(name) {
|
|
178
|
+
const file = await harness.readSkillFile?.(name);
|
|
179
|
+
return file ? { found: true, content: file.content, origin: file.origin } : { found: false };
|
|
180
|
+
},
|
|
181
|
+
async writeSkillFile(name, content) {
|
|
182
|
+
if (!harness.writeSkillFile)
|
|
183
|
+
return { ok: false };
|
|
184
|
+
return harness.writeSkillFile(name, content);
|
|
185
|
+
},
|
|
186
|
+
async copySkillForEdit(name) {
|
|
187
|
+
return { copied: (await harness.copyBaseForEdit?.(name))?.copied ?? false };
|
|
188
|
+
},
|
|
189
|
+
async deleteOverlaySkill(name) {
|
|
190
|
+
return { removed: (await harness.deleteOverlaySkill?.(name))?.removed ?? false };
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function harnessTools(harness) {
|
|
195
|
+
return async () => ({
|
|
196
|
+
tools: ((await harness.tools?.()) ?? []).map((tool) => ({
|
|
197
|
+
...toolReportEntry(tool),
|
|
198
|
+
description: tool.description
|
|
199
|
+
}))
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function toolReportEntry(tool) {
|
|
203
|
+
return {
|
|
204
|
+
name: tool.name,
|
|
205
|
+
promptChars: tool.promptChars ?? 0,
|
|
206
|
+
toolset: tool.toolset ?? null,
|
|
207
|
+
skillGated: tool.skillGated === true,
|
|
208
|
+
scopeRequired: tool.scopeRequired === true,
|
|
209
|
+
rootsRequired: tool.rootsRequired === true,
|
|
210
|
+
writeRootsRequired: tool.writeRootsRequired === true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function harnessSpeakStream(harness) {
|
|
214
|
+
return async function* (text) {
|
|
215
|
+
if (!harness.speakStream)
|
|
216
|
+
throw new Error('harness does not support speakStream');
|
|
217
|
+
for await (const { audio, sampleRate } of harness.speakStream({ model: '', text })) {
|
|
218
|
+
yield { audio, sampleRate };
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function toEngineSkill(skill) {
|
|
223
|
+
const setup = normalizeSkillSetup(skill.setup);
|
|
224
|
+
return {
|
|
225
|
+
name: skill.name,
|
|
226
|
+
description: skill.description,
|
|
227
|
+
tools: [...skill.tools],
|
|
228
|
+
credentials: [...skill.credentials],
|
|
229
|
+
allowList: [...skill.allowList],
|
|
230
|
+
platforms: [...skill.platform],
|
|
231
|
+
status: skill.status,
|
|
232
|
+
...(skill.requires ? { requirements: toEngineSkillRequirements(skill.requires) } : {}),
|
|
233
|
+
...(skill.install ? { install: skill.install.map(toEngineInstallStep) } : {}),
|
|
234
|
+
...(setup ? { setup: toEngineSkillSetup(setup) } : {}),
|
|
235
|
+
...(skill.stub !== undefined ? { stub: skill.stub } : {}),
|
|
236
|
+
...(skill.minCapabilityProfile ? { minCapabilityProfile: skill.minCapabilityProfile } : {}),
|
|
237
|
+
...(skill.emoji ? { emoji: skill.emoji } : {})
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function toEngineSkillRequirements(requires) {
|
|
241
|
+
return {
|
|
242
|
+
binaries: (requires.bins ?? []).map((bin) => ({
|
|
243
|
+
bin,
|
|
244
|
+
minVersion: requires.binMinVersions?.[bin]?.min,
|
|
245
|
+
command: requires.binMinVersions?.[bin]?.command
|
|
246
|
+
})),
|
|
247
|
+
credentials: [...(requires.credentials ?? [])]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function toEngineInstallStep(step) {
|
|
251
|
+
return {
|
|
252
|
+
id: step.id,
|
|
253
|
+
kind: step.kind,
|
|
254
|
+
label: step.label,
|
|
255
|
+
...(step.formula ? { formula: step.formula } : {}),
|
|
256
|
+
...(step.command ? { command: step.command } : {}),
|
|
257
|
+
...(step.manual !== undefined ? { manual: step.manual } : {}),
|
|
258
|
+
...(step.bins ? { bins: [...step.bins] } : {})
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function toEngineSkillSetup(setup) {
|
|
262
|
+
return {
|
|
263
|
+
...(setup.summary ? { summary: setup.summary } : {}),
|
|
264
|
+
...(setup.routes
|
|
265
|
+
? {
|
|
266
|
+
routes: setup.routes.map((route) => ({
|
|
267
|
+
kind: route.kind,
|
|
268
|
+
label: route.label,
|
|
269
|
+
...(route.provider ? { provider: route.provider } : {}),
|
|
270
|
+
...(route.credentialKey ? { credentialKey: route.credentialKey } : {}),
|
|
271
|
+
...(route.description ? { description: route.description } : {}),
|
|
272
|
+
...(route.helpUrl ? { helpUrl: route.helpUrl } : {}),
|
|
273
|
+
...(route.steps ? { steps: [...route.steps] } : {}),
|
|
274
|
+
...(route.configSteps ? { configSteps: [...route.configSteps] } : {}),
|
|
275
|
+
...(route.fields
|
|
276
|
+
? {
|
|
277
|
+
fields: route.fields.map((field) => ({
|
|
278
|
+
key: field.key,
|
|
279
|
+
label: field.label,
|
|
280
|
+
...(field.placeholder ? { placeholder: field.placeholder } : {}),
|
|
281
|
+
...(field.secret !== undefined ? { secret: field.secret } : {}),
|
|
282
|
+
...(field.required !== undefined ? { required: field.required } : {})
|
|
283
|
+
}))
|
|
284
|
+
}
|
|
285
|
+
: {}),
|
|
286
|
+
...(route.satisfied !== undefined ? { satisfied: route.satisfied } : {})
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
: {})
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const FINAL_FLUSH_MS = 10_000;
|
|
293
|
+
const UPLOAD_FRAME_BYTES = 64 * 1024;
|
|
294
|
+
const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
295
|
+
function settledWithin(write, ms) {
|
|
296
|
+
let timer;
|
|
297
|
+
const expired = new Promise((resolve) => {
|
|
298
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
299
|
+
});
|
|
300
|
+
const landed = write.then(() => true, () => true);
|
|
301
|
+
return Promise.race([landed, expired]).finally(() => clearTimeout(timer));
|
|
302
|
+
}
|
|
303
|
+
function isDeniedResult(result) {
|
|
304
|
+
if (!result || typeof result !== 'object')
|
|
305
|
+
return false;
|
|
306
|
+
return result.denied === true;
|
|
307
|
+
}
|
|
308
|
+
const CONSUME_RETRY_MS = 1000;
|
|
309
|
+
const CONSUME_RETRY_MAX_MS = 30000;
|
|
310
|
+
const BLOB_READ_TIMEOUT_MS = 15_000;
|
|
311
|
+
function nextRetryDelay(err, current) {
|
|
312
|
+
return isMeshTransition(err) ? Math.min(current * 2, CONSUME_RETRY_MAX_MS) : CONSUME_RETRY_MS;
|
|
313
|
+
}
|
|
314
|
+
const SUSPENDED = RUN_ABORT_REASONS.suspended;
|
|
315
|
+
const OWNERSHIP_LOST = RUN_ABORT_REASONS.ownershipLost;
|
|
316
|
+
const RUN_SETTLED = RUN_ABORT_REASONS.runSettled;
|
|
317
|
+
const RUNNER_DIED = RUN_ABORT_REASONS.runnerDied;
|
|
318
|
+
const RUNNER_CLOSED = RUN_ABORT_REASONS.runnerClosed;
|
|
319
|
+
const HISTORY_TYPES = HISTORY_CHUNK_TYPES;
|
|
320
|
+
const SKILL_TOOL = 'skill';
|
|
321
|
+
function isSetUpHere(status) {
|
|
322
|
+
return status === SKILL_STATE.ready || status === SKILL_STATE.incompatibleModel;
|
|
323
|
+
}
|
|
324
|
+
function sameNameList(left, right) {
|
|
325
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
326
|
+
}
|
|
327
|
+
function withConfigDefaults(agent, config) {
|
|
328
|
+
if (agent.ctxSize || !config?.ctxSize)
|
|
329
|
+
return agent;
|
|
330
|
+
return { ...agent, ctxSize: config.ctxSize };
|
|
331
|
+
}
|
|
332
|
+
export async function listHistoryChunks(engine, chatId) {
|
|
333
|
+
const [history, toolResults, runStatuses] = await Promise.all([
|
|
334
|
+
engine.listChunks({ chatId, types: HISTORY_TYPES }),
|
|
335
|
+
engine.listChunks({ chatId, type: 'tool-result' }),
|
|
336
|
+
engine.listChunks({ chatId, type: 'run-status' })
|
|
337
|
+
]);
|
|
338
|
+
return [
|
|
339
|
+
...(history.chunks ?? []),
|
|
340
|
+
...(toolResults.chunks ?? []),
|
|
341
|
+
...(runStatuses.chunks ?? [])
|
|
342
|
+
].sort((a, b) => a.seq - b.seq);
|
|
343
|
+
}
|
|
344
|
+
function latestAttemptStarts(chunks) {
|
|
345
|
+
const starts = new Map();
|
|
346
|
+
for (const chunk of chunks) {
|
|
347
|
+
if (chunk.type !== 'run-status' || !chunk.runId)
|
|
348
|
+
continue;
|
|
349
|
+
if (TERMINAL_STATES.has(chunk.runStatus?.state ?? ''))
|
|
350
|
+
continue;
|
|
351
|
+
starts.set(chunk.runId, chunk.seq);
|
|
352
|
+
}
|
|
353
|
+
return starts;
|
|
354
|
+
}
|
|
355
|
+
function beforeLatestAttempt(chunk, starts) {
|
|
356
|
+
if (!chunk.runId)
|
|
357
|
+
return false;
|
|
358
|
+
const start = starts.get(chunk.runId);
|
|
359
|
+
return start !== undefined && chunk.seq < start;
|
|
360
|
+
}
|
|
361
|
+
export function historyFromChunks(chunks, stale = {}) {
|
|
362
|
+
const attemptStarts = latestAttemptStarts(chunks);
|
|
363
|
+
const staleRunIds = stale.runIds ?? new Set();
|
|
364
|
+
const staleQuestionChunkIds = stale.questionChunkIds ?? new Set();
|
|
365
|
+
const folded = foldCompactedChunks(chunks);
|
|
366
|
+
const source = folded.chunks;
|
|
367
|
+
const attachments = [];
|
|
368
|
+
const userMessages = [];
|
|
369
|
+
for (const chunk of source) {
|
|
370
|
+
if (chunk.type === 'attachment')
|
|
371
|
+
attachments.push(chunk);
|
|
372
|
+
else if (chunk.userMessage)
|
|
373
|
+
userMessages.push(chunk);
|
|
374
|
+
}
|
|
375
|
+
const byMessage = groupAttachmentsByMessage(attachments, userMessages);
|
|
376
|
+
const history = [];
|
|
377
|
+
const replayedSkills = new Set();
|
|
378
|
+
if (folded.summary)
|
|
379
|
+
history.push({ role: 'user', content: compactionTurn(folded.summary) });
|
|
380
|
+
const byRun = new Map();
|
|
381
|
+
const anchor = (runId) => {
|
|
382
|
+
let turn = byRun.get(runId);
|
|
383
|
+
if (!turn) {
|
|
384
|
+
turn = { role: 'assistant', content: '' };
|
|
385
|
+
byRun.set(runId, turn);
|
|
386
|
+
history.push(turn);
|
|
387
|
+
}
|
|
388
|
+
return turn;
|
|
389
|
+
};
|
|
390
|
+
for (const chunk of source) {
|
|
391
|
+
if (chunk.userMessage) {
|
|
392
|
+
if (staleQuestionChunkIds.has(chunk.id))
|
|
393
|
+
continue;
|
|
394
|
+
const turn = { role: 'user', content: chunk.userMessage.text };
|
|
395
|
+
const refs = (byMessage.get(chunk.id) ?? [])
|
|
396
|
+
.map((attachmentChunk) => attachmentChunk.attachment)
|
|
397
|
+
.filter((ref) => !ref.deletedAt);
|
|
398
|
+
const images = refs.filter((ref) => IMAGE_MIMES.has(ref.mimeType.toLowerCase()));
|
|
399
|
+
const audio = refs.filter((ref) => isAudioAttachment(ref.fileName, ref.mimeType));
|
|
400
|
+
const documents = refs.filter((ref) => isDocumentAttachment(ref));
|
|
401
|
+
if (images.length)
|
|
402
|
+
turn.images = images;
|
|
403
|
+
if (audio.length)
|
|
404
|
+
turn.audioAttachments = audio;
|
|
405
|
+
if (documents.length)
|
|
406
|
+
turn.documents = documents;
|
|
407
|
+
history.push(turn);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (chunk.attachment)
|
|
411
|
+
continue;
|
|
412
|
+
if (chunk.type === 'run-request' && chunk.runId) {
|
|
413
|
+
anchor(chunk.runId);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (chunk.toolResult?.name === SKILL_TOOL) {
|
|
417
|
+
if (chunk.runId && staleRunIds.has(chunk.runId))
|
|
418
|
+
continue;
|
|
419
|
+
if (beforeLatestAttempt(chunk, attemptStarts))
|
|
420
|
+
continue;
|
|
421
|
+
const loaded = chunk.toolResult.result?.name;
|
|
422
|
+
if (typeof loaded === 'string' && !replayedSkills.has(loaded)) {
|
|
423
|
+
replayedSkills.add(loaded);
|
|
424
|
+
history.push({ role: 'tool', content: skillLoadedTurn(loaded) });
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (chunk.toolResult) {
|
|
429
|
+
if (chunk.runId && staleRunIds.has(chunk.runId))
|
|
430
|
+
continue;
|
|
431
|
+
if (beforeLatestAttempt(chunk, attemptStarts))
|
|
432
|
+
continue;
|
|
433
|
+
const produced = attachmentTurn(chunk.toolResult.name, chunk.toolResult.result);
|
|
434
|
+
if (produced)
|
|
435
|
+
history.push({ role: 'tool', content: produced });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!chunk.content)
|
|
439
|
+
continue;
|
|
440
|
+
if (chunk.runId && staleRunIds.has(chunk.runId))
|
|
441
|
+
continue;
|
|
442
|
+
if (beforeLatestAttempt(chunk, attemptStarts))
|
|
443
|
+
continue;
|
|
444
|
+
anchor(chunk.runId).content += chunk.content.text;
|
|
445
|
+
}
|
|
446
|
+
return history.filter((turn) => turn.role === 'user' || turn.content.length > 0);
|
|
447
|
+
}
|
|
448
|
+
function withoutOccupancy(metrics) {
|
|
449
|
+
if (!metrics)
|
|
450
|
+
return metrics;
|
|
451
|
+
const { cacheTokens, promptTokens, ...rest } = metrics;
|
|
452
|
+
return rest;
|
|
453
|
+
}
|
|
454
|
+
function inputAvailableToRun(chunk, requestSeq) {
|
|
455
|
+
const userInput = Boolean(chunk.userMessage || (chunk.attachment && !chunk.runId));
|
|
456
|
+
return !userInput || chunk.seq < requestSeq;
|
|
457
|
+
}
|
|
458
|
+
function normalizeQuestion(text) {
|
|
459
|
+
return text.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
460
|
+
}
|
|
461
|
+
const GENERIC_ACKNOWLEDGEMENTS = new Set([
|
|
462
|
+
'yes',
|
|
463
|
+
'y',
|
|
464
|
+
'yeah',
|
|
465
|
+
'yep',
|
|
466
|
+
'yup',
|
|
467
|
+
'ok',
|
|
468
|
+
'okay',
|
|
469
|
+
'k',
|
|
470
|
+
'sure',
|
|
471
|
+
'fine',
|
|
472
|
+
'no',
|
|
473
|
+
'n',
|
|
474
|
+
'nope',
|
|
475
|
+
'nah',
|
|
476
|
+
'again',
|
|
477
|
+
'continue',
|
|
478
|
+
'go on',
|
|
479
|
+
'go ahead',
|
|
480
|
+
'keep going',
|
|
481
|
+
'proceed',
|
|
482
|
+
'do it',
|
|
483
|
+
'more',
|
|
484
|
+
'please',
|
|
485
|
+
'thanks',
|
|
486
|
+
'thank you',
|
|
487
|
+
'retry',
|
|
488
|
+
'redo'
|
|
489
|
+
]);
|
|
490
|
+
function isGenericAcknowledgement(question) {
|
|
491
|
+
return GENERIC_ACKNOWLEDGEMENTS.has(question.replace(/[?.!]+$/, '').trim());
|
|
492
|
+
}
|
|
493
|
+
export function staleToolRunIds(historyChunks, approvalChunks, currentRunId) {
|
|
494
|
+
const empty = { runIds: new Set(), questionChunkIds: new Set() };
|
|
495
|
+
const toolRuns = new Set();
|
|
496
|
+
for (const chunk of approvalChunks) {
|
|
497
|
+
if (chunk.runId)
|
|
498
|
+
toolRuns.add(chunk.runId);
|
|
499
|
+
}
|
|
500
|
+
if (!toolRuns.size)
|
|
501
|
+
return empty;
|
|
502
|
+
const turnOrder = [];
|
|
503
|
+
const turnText = new Map();
|
|
504
|
+
const turnRuns = new Map();
|
|
505
|
+
const runChunk = new Map();
|
|
506
|
+
let lastUser = null;
|
|
507
|
+
for (const chunk of historyChunks) {
|
|
508
|
+
if (chunk.userMessage) {
|
|
509
|
+
lastUser = { text: chunk.userMessage.text, chunkId: chunk.id };
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (chunk.type === 'run-request' && chunk.runId && lastUser !== null) {
|
|
513
|
+
const chunkId = lastUser.chunkId;
|
|
514
|
+
if (!turnText.has(chunkId)) {
|
|
515
|
+
turnOrder.push(chunkId);
|
|
516
|
+
turnText.set(chunkId, lastUser.text);
|
|
517
|
+
}
|
|
518
|
+
turnRuns.set(chunkId, [...(turnRuns.get(chunkId) ?? []), chunk.runId]);
|
|
519
|
+
runChunk.set(chunk.runId, chunkId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const currentChunkId = runChunk.get(currentRunId) ?? lastUser?.chunkId;
|
|
523
|
+
if (currentChunkId === undefined)
|
|
524
|
+
return empty;
|
|
525
|
+
const currentText = turnText.get(currentChunkId) ?? lastUser?.text;
|
|
526
|
+
if (currentText === undefined)
|
|
527
|
+
return empty;
|
|
528
|
+
const target = normalizeQuestion(currentText);
|
|
529
|
+
if (isGenericAcknowledgement(target))
|
|
530
|
+
return empty;
|
|
531
|
+
const currentIndex = turnOrder.indexOf(currentChunkId);
|
|
532
|
+
const start = currentIndex === -1 ? turnOrder.length - 1 : currentIndex - 1;
|
|
533
|
+
const runIds = new Set();
|
|
534
|
+
const questionChunkIds = new Set();
|
|
535
|
+
let blockHadApproval = false;
|
|
536
|
+
for (let i = start; i >= 0; i--) {
|
|
537
|
+
const chunkId = turnOrder[i];
|
|
538
|
+
if (normalizeQuestion(turnText.get(chunkId)) !== target)
|
|
539
|
+
break;
|
|
540
|
+
questionChunkIds.add(chunkId);
|
|
541
|
+
for (const rid of turnRuns.get(chunkId) ?? []) {
|
|
542
|
+
runIds.add(rid);
|
|
543
|
+
if (toolRuns.has(rid))
|
|
544
|
+
blockHadApproval = true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!blockHadApproval)
|
|
548
|
+
return empty;
|
|
549
|
+
return { runIds, questionChunkIds };
|
|
550
|
+
}
|
|
551
|
+
export class QvacAssistant extends ReadyResource {
|
|
552
|
+
_engine;
|
|
553
|
+
_harness;
|
|
554
|
+
_log;
|
|
555
|
+
_imageCacheDir;
|
|
556
|
+
_imageCacheMaxBytes;
|
|
557
|
+
_imageCacheMaxAgeMs;
|
|
558
|
+
_downloadRetryMs;
|
|
559
|
+
_platform;
|
|
560
|
+
_mobileClass;
|
|
561
|
+
_preloadCurrentAgentModel;
|
|
562
|
+
_drives;
|
|
563
|
+
_blobReadTimeoutMs;
|
|
564
|
+
_warmAbort = new AbortController();
|
|
565
|
+
_warmedModel = false;
|
|
566
|
+
_warmedModelName = null;
|
|
567
|
+
_modelStatusLogged = new Map();
|
|
568
|
+
_deviceId;
|
|
569
|
+
_info;
|
|
570
|
+
_lastCapabilities;
|
|
571
|
+
_readySkills;
|
|
572
|
+
_recordingReadiness = false;
|
|
573
|
+
_readinessDirty = false;
|
|
574
|
+
_reconciling;
|
|
575
|
+
_onlineBoot = new ReadyGuard();
|
|
576
|
+
_capability;
|
|
577
|
+
_downloads;
|
|
578
|
+
_state;
|
|
579
|
+
_harnessDiagnostics;
|
|
580
|
+
_harnessDeath;
|
|
581
|
+
_refreshingDiagnostics;
|
|
582
|
+
_requests;
|
|
583
|
+
_retry;
|
|
584
|
+
_retryMs = CONSUME_RETRY_MS;
|
|
585
|
+
_chatList;
|
|
586
|
+
_agentList;
|
|
587
|
+
_chatListWatch;
|
|
588
|
+
_agentWatch;
|
|
589
|
+
_runs;
|
|
590
|
+
_seen;
|
|
591
|
+
_lifecycle;
|
|
592
|
+
_audioTranscripts;
|
|
593
|
+
_chats;
|
|
594
|
+
_runImageCacheNames;
|
|
595
|
+
_reconciledCompactions;
|
|
596
|
+
_modelCatalog = new ModelCatalogRevisionHub();
|
|
597
|
+
_onHarnessStatus;
|
|
598
|
+
_onModelCatalogEvent;
|
|
599
|
+
constructor(engine, harness, { logger = null, imageCacheDir, imageCacheMaxBytes, imageCacheMaxAgeMs, downloadRetryMs, platform = os.platform(), preloadCurrentAgentModel = true, drives, blobReadTimeoutMs = BLOB_READ_TIMEOUT_MS } = {}) {
|
|
600
|
+
super();
|
|
601
|
+
this._engine = engine;
|
|
602
|
+
this._harness = harness;
|
|
603
|
+
this._drives = drives;
|
|
604
|
+
this._log = logger;
|
|
605
|
+
this._imageCacheDir = imageCacheDir ?? defaultImageCacheDir();
|
|
606
|
+
this._imageCacheMaxBytes = imageCacheMaxBytes;
|
|
607
|
+
this._imageCacheMaxAgeMs = imageCacheMaxAgeMs;
|
|
608
|
+
this._downloadRetryMs = downloadRetryMs;
|
|
609
|
+
this._platform = platform;
|
|
610
|
+
this._mobileClass = isMobilePlatform(platform);
|
|
611
|
+
this._preloadCurrentAgentModel = preloadCurrentAgentModel;
|
|
612
|
+
this._blobReadTimeoutMs = blobReadTimeoutMs;
|
|
613
|
+
this._deviceId = null;
|
|
614
|
+
this._info = { toolNames: [], skills: [], supportsImages: false };
|
|
615
|
+
this._lastCapabilities = null;
|
|
616
|
+
this._readySkills = null;
|
|
617
|
+
this._reconciling = false;
|
|
618
|
+
this._capability = null;
|
|
619
|
+
this._downloads = null;
|
|
620
|
+
this._state = QvacState.RESUME;
|
|
621
|
+
this._harnessDiagnostics = null;
|
|
622
|
+
this._harnessDeath = null;
|
|
623
|
+
this._refreshingDiagnostics = false;
|
|
624
|
+
this._requests = null;
|
|
625
|
+
this._retry = null;
|
|
626
|
+
this._chatList = null;
|
|
627
|
+
this._agentList = null;
|
|
628
|
+
this._chatListWatch = this._rearmingWatch('chat-list', () => this._watchChatList());
|
|
629
|
+
this._agentWatch = this._rearmingWatch('agent', () => this._watchAgents());
|
|
630
|
+
this._runs = new Map();
|
|
631
|
+
this._seen = new Set();
|
|
632
|
+
this._lifecycle = new AbortController();
|
|
633
|
+
this._audioTranscripts = new Map();
|
|
634
|
+
this._chats = new Map();
|
|
635
|
+
this._runImageCacheNames = new Map();
|
|
636
|
+
this._reconciledCompactions = new Map();
|
|
637
|
+
this._onHarnessStatus = (status) => {
|
|
638
|
+
this._log?.warn('assistant', 'harness status', status);
|
|
639
|
+
if (harnessDied(status)) {
|
|
640
|
+
this._harnessDeath = harnessDeathReason(status);
|
|
641
|
+
for (const run of this._runs.values())
|
|
642
|
+
run.controller.abort(RUNNER_DIED);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
this._harnessDeath = null;
|
|
646
|
+
}
|
|
647
|
+
this.emit('harness-status', status);
|
|
648
|
+
};
|
|
649
|
+
this._onModelCatalogEvent = (state) => {
|
|
650
|
+
this._logModelStatus(state);
|
|
651
|
+
if (modelCatalogChanged(state))
|
|
652
|
+
this._modelCatalog.notify();
|
|
653
|
+
const progress = imageDownloadProgress(state);
|
|
654
|
+
if (progress)
|
|
655
|
+
this._downloads?.observeImageProgress(progress);
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async _open() {
|
|
659
|
+
this._log?.info('assistant', 'host platform resolved', {
|
|
660
|
+
platform: this._platform,
|
|
661
|
+
mobileClass: this._mobileClass
|
|
662
|
+
});
|
|
663
|
+
const info = await this._engine.deviceInfo({});
|
|
664
|
+
this._deviceId = info.id;
|
|
665
|
+
this._harness.on?.('status', this._onHarnessStatus);
|
|
666
|
+
this._harness.on?.('model', this._onModelCatalogEvent);
|
|
667
|
+
this._info = await this._describeHarness();
|
|
668
|
+
await this._clearPersistedPauses();
|
|
669
|
+
this._downloads = new ModelDownloadReconciler({
|
|
670
|
+
engine: this._engine,
|
|
671
|
+
harness: this._harness,
|
|
672
|
+
deviceId: this._deviceId,
|
|
673
|
+
onCacheChanged: () => {
|
|
674
|
+
this._modelCatalog.notify();
|
|
675
|
+
void this._reportCapabilities();
|
|
676
|
+
this._triggerModelWarm();
|
|
677
|
+
},
|
|
678
|
+
retryMs: this._downloadRetryMs,
|
|
679
|
+
logger: this._log,
|
|
680
|
+
mobileClass: this._mobileClass
|
|
681
|
+
});
|
|
682
|
+
this._downloads.start();
|
|
683
|
+
this._startConsume();
|
|
684
|
+
this._chatListWatch.start();
|
|
685
|
+
this._agentWatch.start();
|
|
686
|
+
void this._bootOnline();
|
|
687
|
+
this._triggerModelWarm();
|
|
688
|
+
void this.onlineReady().then(() => this._triggerModelWarm(), noop);
|
|
689
|
+
}
|
|
690
|
+
_triggerModelWarm() {
|
|
691
|
+
if (!this._preloadCurrentAgentModel)
|
|
692
|
+
return;
|
|
693
|
+
void this._warmCurrentAgentModel().catch((err) => this._log?.warn('assistant', 'current agent model warm failed', err));
|
|
694
|
+
}
|
|
695
|
+
async _warmCurrentAgentModel() {
|
|
696
|
+
if (this._warmedModel)
|
|
697
|
+
return;
|
|
698
|
+
const deviceId = this._deviceId;
|
|
699
|
+
const config = await this._engine.getConfig({}).catch(() => null);
|
|
700
|
+
const agent = await this._currentAgent(config?.activeAgentId ?? null, deviceId);
|
|
701
|
+
if (!agent?.modelName || !agent.providerDeviceId?.equals(deviceId) || this.suspended)
|
|
702
|
+
return;
|
|
703
|
+
const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
|
|
704
|
+
const rows = await this._harness.list?.({ cachedOnly: true }).catch(() => null);
|
|
705
|
+
if (!rows?.some((row) => row.name === model && row.isCached)) {
|
|
706
|
+
this._log?.info('models', 'model warm skipped', { model, reason: 'not cached' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (this._warmedModel)
|
|
710
|
+
return;
|
|
711
|
+
this._warmedModel = true;
|
|
712
|
+
this._warmedModelName = model;
|
|
713
|
+
this._log?.info('models', 'model warm started', { model, agentModel: agent.modelName });
|
|
714
|
+
const started = Date.now();
|
|
715
|
+
await this._harness.warmModel?.({
|
|
716
|
+
model,
|
|
717
|
+
agent: withConfigDefaults(agent, config),
|
|
718
|
+
signal: this._warmAbort.signal
|
|
719
|
+
});
|
|
720
|
+
this._log?.info('models', 'model warm done', { model, ms: Date.now() - started });
|
|
721
|
+
}
|
|
722
|
+
async _currentAgent(activeAgentId, deviceId) {
|
|
723
|
+
if (activeAgentId) {
|
|
724
|
+
const agent = await this._engine.getAgent({ id: activeAgentId }).catch(() => null);
|
|
725
|
+
if (agent && !agent.deletedAt)
|
|
726
|
+
return agent;
|
|
727
|
+
}
|
|
728
|
+
const { agents } = await this._engine.listAgents({}).catch(() => ({ agents: [] }));
|
|
729
|
+
const all = agents ?? [];
|
|
730
|
+
return (all.find((agent) => agent.isDefault && agent.providerDeviceId?.equals(deviceId)) ??
|
|
731
|
+
all.find((agent) => agent.isDefault) ??
|
|
732
|
+
all[0] ??
|
|
733
|
+
null);
|
|
734
|
+
}
|
|
735
|
+
async _bootOnline() {
|
|
736
|
+
void this._onlineBoot.ready().catch(noop);
|
|
737
|
+
try {
|
|
738
|
+
await Promise.allSettled([
|
|
739
|
+
this._sweepImageCache(),
|
|
740
|
+
this._reportCapabilities(),
|
|
741
|
+
this._reconcileDeviceProfile()
|
|
742
|
+
]);
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
this._onlineBoot.exit();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async onlineReady() {
|
|
749
|
+
if (!this.opened)
|
|
750
|
+
await this.ready();
|
|
751
|
+
await this._onlineBoot.ready();
|
|
752
|
+
}
|
|
753
|
+
async _clearPersistedPauses() {
|
|
754
|
+
try {
|
|
755
|
+
const config = await this._engine.getConfig({});
|
|
756
|
+
for (const name of config.pausedModels ?? []) {
|
|
757
|
+
await this._engine.setModelPaused({ name, paused: false });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
this._log?.warn('assistant', 'clearing persisted model pauses failed', err);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async ensureMediaModelUnpaused(category, model) {
|
|
765
|
+
try {
|
|
766
|
+
const name = model || mediaModelNameFor(category, await this._persistedProfile());
|
|
767
|
+
const config = await this._engine.getConfig({});
|
|
768
|
+
if (!(config.pausedModels ?? []).includes(name))
|
|
769
|
+
return true;
|
|
770
|
+
await this._engine.setModelPaused({ name, paused: false });
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
this._log?.warn('assistant', 'ensuring media model unpaused failed', err);
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async _persistedProfile() {
|
|
779
|
+
const deviceId = this._deviceId;
|
|
780
|
+
if (!deviceId)
|
|
781
|
+
return 'mid';
|
|
782
|
+
const { devices } = await this._engine.listDevices({}).catch(() => ({ devices: [] }));
|
|
783
|
+
const self = (devices ?? []).find((device) => device.id.equals(deviceId));
|
|
784
|
+
return self?.capabilityProfile ?? 'mid';
|
|
785
|
+
}
|
|
786
|
+
async _sweepImageCache() {
|
|
787
|
+
try {
|
|
788
|
+
const keep = await this._referencedImageNames();
|
|
789
|
+
await sweepImageCache(this._imageCacheDir, {
|
|
790
|
+
maxBytes: this._imageCacheMaxBytes,
|
|
791
|
+
maxAgeMs: this._imageCacheMaxAgeMs,
|
|
792
|
+
inUse: (name) => this._imageCacheInUse(name),
|
|
793
|
+
...(keep ? { keep } : {})
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
this._log?.warn('assistant', 'image cache sweep failed', err);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
_imageCacheInUse(name) {
|
|
801
|
+
for (const names of this._runImageCacheNames.values())
|
|
802
|
+
if (names.has(name))
|
|
803
|
+
return true;
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
async _referencedImageNames() {
|
|
807
|
+
try {
|
|
808
|
+
const { chats } = await this._engine.listChats({});
|
|
809
|
+
if (!chats?.length)
|
|
810
|
+
return null;
|
|
811
|
+
const keep = new Set();
|
|
812
|
+
for (const chat of chats) {
|
|
813
|
+
const { chunks } = await this._engine.listChunks({ chatId: chat.id, type: 'attachment' });
|
|
814
|
+
for (const chunk of chunks ?? []) {
|
|
815
|
+
const attachment = chunk.attachment;
|
|
816
|
+
if (attachment && !attachment.deletedAt)
|
|
817
|
+
keep.add(imageCacheName(attachment));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return keep;
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async _describeHarness() {
|
|
827
|
+
const described = await this._harness.describe?.();
|
|
828
|
+
return (described ?? {
|
|
829
|
+
toolNames: this._harness.toolNames ?? [],
|
|
830
|
+
skills: this._harness.skillNames ?? [],
|
|
831
|
+
supportsImages: this._harness.supportsImages ?? false
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
diagnosticsSlice() {
|
|
835
|
+
const harness = this._harness.diagnostics?.() ?? this._harnessDiagnostics;
|
|
836
|
+
void this._refreshHarnessDiagnostics();
|
|
837
|
+
const runs = [...this._runs.entries()].map(([runId, run]) => ({
|
|
838
|
+
runId,
|
|
839
|
+
chatId: run.chatId,
|
|
840
|
+
model: run.model
|
|
841
|
+
}));
|
|
842
|
+
return {
|
|
843
|
+
harness,
|
|
844
|
+
assistant: summarizeRuns(runs, this.suspended),
|
|
845
|
+
platform: this._platform,
|
|
846
|
+
mobileClass: this._mobileClass,
|
|
847
|
+
memoryBudget: this._capability?.memoryBudget ?? null
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
async _refreshHarnessDiagnostics() {
|
|
851
|
+
if (this._refreshingDiagnostics || !this._harness.fetchDiagnostics)
|
|
852
|
+
return;
|
|
853
|
+
this._refreshingDiagnostics = true;
|
|
854
|
+
try {
|
|
855
|
+
this._harnessDiagnostics = await this._harness.fetchDiagnostics();
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
}
|
|
859
|
+
finally {
|
|
860
|
+
this._refreshingDiagnostics = false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async _reportCapabilities() {
|
|
864
|
+
let models;
|
|
865
|
+
try {
|
|
866
|
+
models = (await this._harness.list?.()) ?? [];
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
this._log?.warn('assistant', '_reportCapabilities: harness.list failed — keeping last catalog', err);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const installedModelNames = models.map((model) => model.name);
|
|
873
|
+
const cachedModelNames = models.filter((model) => model.isCached).map((model) => model.name);
|
|
874
|
+
let toolCatalog = null;
|
|
875
|
+
if (this._harness.tools) {
|
|
876
|
+
try {
|
|
877
|
+
toolCatalog = (await this._harness.tools()).map(toolReportEntry);
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
this._log?.warn('assistant', '_reportCapabilities: harness.tools failed', err);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const key = JSON.stringify([
|
|
884
|
+
this._info.toolNames,
|
|
885
|
+
this._info.skills,
|
|
886
|
+
installedModelNames,
|
|
887
|
+
cachedModelNames,
|
|
888
|
+
this._readySkills,
|
|
889
|
+
toolCatalog
|
|
890
|
+
]);
|
|
891
|
+
if (key === this._lastCapabilities)
|
|
892
|
+
return;
|
|
893
|
+
try {
|
|
894
|
+
await this._engine.saveDeviceCapabilities({
|
|
895
|
+
installedToolNames: this._info.toolNames,
|
|
896
|
+
installedSkillNames: this._info.skills,
|
|
897
|
+
installedModelNames,
|
|
898
|
+
cachedModelNames,
|
|
899
|
+
...(this._readySkills ? { readySkillNames: this._readySkills } : {}),
|
|
900
|
+
...(toolCatalog ? { toolCatalog } : {})
|
|
901
|
+
});
|
|
902
|
+
this._lastCapabilities = key;
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
if (isMeshTransition(err))
|
|
906
|
+
return;
|
|
907
|
+
this._log?.error('assistant', 'saveDeviceCapabilities failed', err);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async _ensureCapability() {
|
|
911
|
+
if (this._capability)
|
|
912
|
+
return this._capability;
|
|
913
|
+
let probed;
|
|
914
|
+
try {
|
|
915
|
+
probed = await this._harness.capability?.();
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
this._log?.warn('assistant', '_reconcileDeviceProfile: harness.capability failed', err);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
if (!probed?.recommendedModelName)
|
|
922
|
+
return null;
|
|
923
|
+
this._capability = probed;
|
|
924
|
+
return probed;
|
|
925
|
+
}
|
|
926
|
+
async _reconcileDeviceProfile() {
|
|
927
|
+
const deviceId = this._deviceId;
|
|
928
|
+
if (!deviceId || this._reconciling)
|
|
929
|
+
return;
|
|
930
|
+
this._reconciling = true;
|
|
931
|
+
try {
|
|
932
|
+
const capability = await this._ensureCapability();
|
|
933
|
+
if (!capability)
|
|
934
|
+
return;
|
|
935
|
+
const { devices } = await this._engine.listDevices({}).catch(() => ({ devices: [] }));
|
|
936
|
+
const self = (devices ?? []).find((device) => device.id.equals(deviceId));
|
|
937
|
+
if (!self)
|
|
938
|
+
return;
|
|
939
|
+
const profileUpdated = !(self.capabilityProfile === capability.capabilityProfile &&
|
|
940
|
+
self.recommendedModelName === capability.recommendedModelName &&
|
|
941
|
+
self.platform === this._platform);
|
|
942
|
+
const profile = {
|
|
943
|
+
publicKey: b4a.toString(deviceId, 'hex'),
|
|
944
|
+
name: self.name,
|
|
945
|
+
platform: this._platform,
|
|
946
|
+
mobileClass: this._mobileClass,
|
|
947
|
+
capabilityProfile: capability.capabilityProfile,
|
|
948
|
+
recommendedModelName: capability.recommendedModelName,
|
|
949
|
+
...(capability.memoryBudget ? { memoryBudget: capability.memoryBudget } : {})
|
|
950
|
+
};
|
|
951
|
+
if (profileUpdated) {
|
|
952
|
+
try {
|
|
953
|
+
await this._engine.saveDeviceProfile({
|
|
954
|
+
capabilityProfile: capability.capabilityProfile,
|
|
955
|
+
recommendedModelName: capability.recommendedModelName,
|
|
956
|
+
platform: this._platform
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
this._log?.error('assistant', 'saveDeviceProfile failed', profile, err);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
this._log?.info('assistant', 'device profile resolved', { ...profile, profileUpdated });
|
|
965
|
+
}
|
|
966
|
+
finally {
|
|
967
|
+
this._reconciling = false;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async _close() {
|
|
971
|
+
if (this._retry)
|
|
972
|
+
clearTimeout(this._retry);
|
|
973
|
+
this._chatListWatch.stop();
|
|
974
|
+
this._agentWatch.stop();
|
|
975
|
+
this._harness.off?.('status', this._onHarnessStatus);
|
|
976
|
+
this._harness.off?.('model', this._onModelCatalogEvent);
|
|
977
|
+
this._warmAbort.abort(RUNNER_CLOSED);
|
|
978
|
+
this._onlineBoot.destroy(new Error('assistant is closing'));
|
|
979
|
+
for (const run of this._runs.values())
|
|
980
|
+
run.controller.abort(RUNNER_CLOSED);
|
|
981
|
+
await this._downloads?.close();
|
|
982
|
+
this._modelCatalog.close();
|
|
983
|
+
await this._requests?.return?.();
|
|
984
|
+
await this._chatList?.return?.();
|
|
985
|
+
await this._agentList?.return?.();
|
|
986
|
+
this._lifecycle.abort(RUNNER_CLOSED);
|
|
987
|
+
await Promise.allSettled([...this._runs.values()].map((run) => run.done));
|
|
988
|
+
}
|
|
989
|
+
get suspended() {
|
|
990
|
+
return this._state === QvacState.SUSPEND;
|
|
991
|
+
}
|
|
992
|
+
async presuspend() {
|
|
993
|
+
if (this.closing)
|
|
994
|
+
return;
|
|
995
|
+
this._state = QvacState.PRESUSPEND;
|
|
996
|
+
this._log?.info('assistant', 'presuspend');
|
|
997
|
+
}
|
|
998
|
+
async suspend() {
|
|
999
|
+
this._log?.info('assistant', 'suspend called');
|
|
1000
|
+
if (this._state === QvacState.SUSPEND)
|
|
1001
|
+
return;
|
|
1002
|
+
if (this.closing)
|
|
1003
|
+
return;
|
|
1004
|
+
this._state = QvacState.SUSPEND;
|
|
1005
|
+
this._log?.info('assistant', 'suspending', { runs: this._runs.size });
|
|
1006
|
+
if (this._retry)
|
|
1007
|
+
clearTimeout(this._retry);
|
|
1008
|
+
this._chatListWatch.stop();
|
|
1009
|
+
this._agentWatch.stop();
|
|
1010
|
+
await this._downloads?.suspend();
|
|
1011
|
+
await this._requests?.return?.();
|
|
1012
|
+
await this._chatList?.return?.();
|
|
1013
|
+
await this._agentList?.return?.();
|
|
1014
|
+
for (const run of this._runs.values())
|
|
1015
|
+
run.controller.abort(SUSPENDED);
|
|
1016
|
+
this._warmedModel = false;
|
|
1017
|
+
this._warmedModelName = null;
|
|
1018
|
+
this._modelCatalog.notify();
|
|
1019
|
+
}
|
|
1020
|
+
async wakeup() {
|
|
1021
|
+
if (this.closing)
|
|
1022
|
+
return;
|
|
1023
|
+
this._state = QvacState.WAKEUP;
|
|
1024
|
+
this._log?.info('assistant', 'wakeup');
|
|
1025
|
+
await this.resume();
|
|
1026
|
+
this._triggerModelWarm();
|
|
1027
|
+
}
|
|
1028
|
+
async resume() {
|
|
1029
|
+
this._log?.info('assistant', 'resume called');
|
|
1030
|
+
if (this._state === QvacState.RESUME)
|
|
1031
|
+
return;
|
|
1032
|
+
if (this.closing)
|
|
1033
|
+
return;
|
|
1034
|
+
if (this._state === QvacState.PRESUSPEND) {
|
|
1035
|
+
this._state = QvacState.RESUME;
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
this._state = QvacState.RESUME;
|
|
1039
|
+
this._log?.info('assistant', 'resuming');
|
|
1040
|
+
await Promise.allSettled([...this._runs.values()].map((run) => run.done));
|
|
1041
|
+
this._downloads?.resume();
|
|
1042
|
+
if (this.opened) {
|
|
1043
|
+
this._startConsume();
|
|
1044
|
+
this._chatListWatch.start();
|
|
1045
|
+
this._agentWatch.start();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async listModels(options) {
|
|
1049
|
+
return (await this._harness.list?.(options)) ?? [];
|
|
1050
|
+
}
|
|
1051
|
+
async deleteModel(name) {
|
|
1052
|
+
return (await this._harness.deleteModel?.(name)) ?? { name, removed: 0, freedBytes: 0 };
|
|
1053
|
+
}
|
|
1054
|
+
modelCatalogWatch() {
|
|
1055
|
+
return this._modelCatalog.watch();
|
|
1056
|
+
}
|
|
1057
|
+
async retryModelDownload(name) {
|
|
1058
|
+
if (!this._downloads)
|
|
1059
|
+
throw new Error('model download reconciler is unavailable');
|
|
1060
|
+
await this._downloads.retry(name);
|
|
1061
|
+
}
|
|
1062
|
+
async skills(agentId, modelName, options) {
|
|
1063
|
+
if (!this.opened)
|
|
1064
|
+
await this.ready();
|
|
1065
|
+
if (agentId) {
|
|
1066
|
+
const agent = await this._engine.getAgent({ id: agentId }).catch(() => null);
|
|
1067
|
+
const deviceId = this._deviceId;
|
|
1068
|
+
if (agent && deviceId && !agent.providerDeviceId?.equals(deviceId)) {
|
|
1069
|
+
agentId = null;
|
|
1070
|
+
options = { ...options, skipCredentialProbe: true };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const resolved = (await this._harness.skills?.(agentId, modelName, options)) ?? [];
|
|
1074
|
+
this._recordSkillReadiness().catch(noop);
|
|
1075
|
+
return resolved;
|
|
1076
|
+
}
|
|
1077
|
+
async _recordSkillReadiness() {
|
|
1078
|
+
this._readinessDirty = true;
|
|
1079
|
+
if (this._recordingReadiness)
|
|
1080
|
+
return;
|
|
1081
|
+
this._recordingReadiness = true;
|
|
1082
|
+
try {
|
|
1083
|
+
while (this._readinessDirty) {
|
|
1084
|
+
this._readinessDirty = false;
|
|
1085
|
+
const unscoped = (await this._harness.skills?.(null, null, { skipCredentialProbe: true })) ?? [];
|
|
1086
|
+
const ready = unscoped
|
|
1087
|
+
.filter((skill) => isSetUpHere(skill.status))
|
|
1088
|
+
.map((skill) => skill.name);
|
|
1089
|
+
const deviceId = this._deviceId;
|
|
1090
|
+
if (deviceId) {
|
|
1091
|
+
const { agents } = await this._engine.listAgents({});
|
|
1092
|
+
const local = (agents ?? []).filter((agent) => !agent.deletedAt && agent.providerDeviceId?.equals(deviceId));
|
|
1093
|
+
for (const agent of local) {
|
|
1094
|
+
const resolved = (await this._harness.skills?.(agent.id, agent.modelName, {
|
|
1095
|
+
skipCredentialProbe: true
|
|
1096
|
+
})) ?? [];
|
|
1097
|
+
ready.push(...encodeAgentSkillReadiness(agent.id, resolved.filter((skill) => isSetUpHere(skill.status)).map((skill) => skill.name)));
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
ready.sort();
|
|
1101
|
+
if (this._readySkills && sameNameList(this._readySkills, ready))
|
|
1102
|
+
continue;
|
|
1103
|
+
this._readySkills = ready;
|
|
1104
|
+
await this._reportCapabilities();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
finally {
|
|
1108
|
+
this._recordingReadiness = false;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async tools() {
|
|
1112
|
+
return (await this._harness.tools?.()) ?? [];
|
|
1113
|
+
}
|
|
1114
|
+
_startConsume() {
|
|
1115
|
+
this._consume().then(() => this._rearmConsume(null), (err) => this._rearmConsume(err));
|
|
1116
|
+
}
|
|
1117
|
+
_rearmingWatch(label, run) {
|
|
1118
|
+
let retryMs = CONSUME_RETRY_MS;
|
|
1119
|
+
let timer = null;
|
|
1120
|
+
const rearm = (err) => {
|
|
1121
|
+
if (this.closing || this.suspended)
|
|
1122
|
+
return;
|
|
1123
|
+
if (err !== null && !isMeshTransition(err)) {
|
|
1124
|
+
this._log?.error('assistant', `${label} watch failed — restarting`, err);
|
|
1125
|
+
}
|
|
1126
|
+
const delay = isMeshTransition(err) ? retryMs : CONSUME_RETRY_MS;
|
|
1127
|
+
retryMs = nextRetryDelay(err, delay);
|
|
1128
|
+
timer = setTimeout(start, delay);
|
|
1129
|
+
};
|
|
1130
|
+
const start = () => {
|
|
1131
|
+
run().then(() => rearm(null), (err) => rearm(err));
|
|
1132
|
+
};
|
|
1133
|
+
return {
|
|
1134
|
+
start,
|
|
1135
|
+
stop: () => {
|
|
1136
|
+
if (timer)
|
|
1137
|
+
clearTimeout(timer);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async _watchChatList() {
|
|
1142
|
+
const stream = this._engine.chatsWatch({});
|
|
1143
|
+
this._chatList = stream[Symbol.asyncIterator]();
|
|
1144
|
+
let known = null;
|
|
1145
|
+
while (true) {
|
|
1146
|
+
const { value, done } = await this._chatList.next();
|
|
1147
|
+
if (done || this.closing || this.suspended)
|
|
1148
|
+
return;
|
|
1149
|
+
const ids = (value.chats ?? []).map((chat) => chat.id);
|
|
1150
|
+
if (known !== null) {
|
|
1151
|
+
for (const chatId of disappearedChatIds(known, ids)) {
|
|
1152
|
+
this._reconciledCompactions.delete(chatId);
|
|
1153
|
+
void this._deleteChatKvCache(chatId, 'chat deleted');
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
known = new Set(ids);
|
|
1157
|
+
await this._sweepImageCache();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async _watchAgents() {
|
|
1161
|
+
const stream = this._engine.agentsWatch({});
|
|
1162
|
+
this._agentList = stream[Symbol.asyncIterator]();
|
|
1163
|
+
let known = null;
|
|
1164
|
+
while (true) {
|
|
1165
|
+
const { value, done } = await this._agentList.next();
|
|
1166
|
+
if (done || this.closing || this.suspended)
|
|
1167
|
+
return;
|
|
1168
|
+
const agents = value.agents ?? [];
|
|
1169
|
+
const next = new Map(agents.map((agent) => [agent.id, agentRekeyFingerprint(agent)]));
|
|
1170
|
+
if (known !== null) {
|
|
1171
|
+
for (const agent of agents) {
|
|
1172
|
+
const previous = known.get(agent.id);
|
|
1173
|
+
if (previous !== undefined && previous !== next.get(agent.id)) {
|
|
1174
|
+
void this._reclaimAgentChatCaches(agent.id);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
known = next;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async _reclaimAgentChatCaches(agentId) {
|
|
1182
|
+
try {
|
|
1183
|
+
const { chats } = await this._engine.listChats({});
|
|
1184
|
+
for (const chat of chats ?? []) {
|
|
1185
|
+
if (chat.agentId === agentId)
|
|
1186
|
+
await this._deleteChatKvCache(chat.id, 'agent re-key');
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
this._log?.warn('assistant', 'agent re-key cache reclaim failed', { agentId, err });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async _reconcileCompaction(chatId) {
|
|
1194
|
+
const deviceId = this._deviceId;
|
|
1195
|
+
const { chunks } = await this._engine
|
|
1196
|
+
.listChunks({ chatId, type: 'compaction' })
|
|
1197
|
+
.catch(() => ({ chunks: [] }));
|
|
1198
|
+
let newest = null;
|
|
1199
|
+
for (const chunk of chunks ?? [])
|
|
1200
|
+
if (!newest || chunk.seq > newest.seq)
|
|
1201
|
+
newest = chunk;
|
|
1202
|
+
if (!newest || newest.originDeviceId?.equals(deviceId))
|
|
1203
|
+
return;
|
|
1204
|
+
if (this._reconciledCompactions.get(chatId) === newest.id)
|
|
1205
|
+
return;
|
|
1206
|
+
const gone = await this._deleteChatKvCache(chatId, 'compaction from another device');
|
|
1207
|
+
if (gone)
|
|
1208
|
+
this._reconciledCompactions.set(chatId, newest.id);
|
|
1209
|
+
}
|
|
1210
|
+
async deleteChatKvCache(chatId) {
|
|
1211
|
+
return ((await this._harness.deleteKvCache?.(chatKvCacheKey(chatId))) ?? {
|
|
1212
|
+
status: 'unsupported',
|
|
1213
|
+
error: 'Harness does not support KV cache deletion'
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
async _deleteChatKvCache(chatId, reason) {
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await this.deleteChatKvCache(chatId);
|
|
1219
|
+
if (result.status === 'failure') {
|
|
1220
|
+
this._log?.warn('assistant', 'chat kv-cache deletion failed', {
|
|
1221
|
+
chatId,
|
|
1222
|
+
reason,
|
|
1223
|
+
error: result.error
|
|
1224
|
+
});
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
catch (err) {
|
|
1230
|
+
this._log?.warn('assistant', 'chat kv-cache deletion failed', { chatId, reason, err });
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
_rearmConsume(err) {
|
|
1235
|
+
if (this.closing || this.suspended)
|
|
1236
|
+
return;
|
|
1237
|
+
if (err !== null && !isMeshTransition(err)) {
|
|
1238
|
+
this._log?.error('assistant', 'run-request watch failed — restarting', err);
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
this._log?.debug('assistant', 'rearming consume', {
|
|
1242
|
+
cause: err === null ? 'stream end' : 'mesh transition'
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
this._lastCapabilities = null;
|
|
1246
|
+
const delay = isMeshTransition(err) ? this._retryMs : CONSUME_RETRY_MS;
|
|
1247
|
+
this._retryMs = nextRetryDelay(err, delay);
|
|
1248
|
+
this._retry = setTimeout(() => this._startConsume(), delay);
|
|
1249
|
+
}
|
|
1250
|
+
async _consume() {
|
|
1251
|
+
const stream = this._engine.runRequestsWatch({});
|
|
1252
|
+
this._requests = stream[Symbol.asyncIterator]();
|
|
1253
|
+
this._reportCapabilities().catch(noop);
|
|
1254
|
+
this._reconcileDeviceProfile().catch(noop);
|
|
1255
|
+
while (true) {
|
|
1256
|
+
const { value, done } = await this._requests.next();
|
|
1257
|
+
if (done || this.closing || this.suspended)
|
|
1258
|
+
return;
|
|
1259
|
+
const requests = value.chunks ?? [];
|
|
1260
|
+
for (const chunk of requests)
|
|
1261
|
+
this._dispatch(chunk);
|
|
1262
|
+
const live = new Set(requests.map((c) => c.runId));
|
|
1263
|
+
for (const id of this._seen)
|
|
1264
|
+
if (!live.has(id))
|
|
1265
|
+
this._seen.delete(id);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
_dispatch(request) {
|
|
1269
|
+
const runId = request.runId;
|
|
1270
|
+
if (!runId || this._runs.has(runId) || this._seen.has(runId))
|
|
1271
|
+
return;
|
|
1272
|
+
this._seen.add(runId);
|
|
1273
|
+
const settle = (async () => {
|
|
1274
|
+
const agentId = request.runRequest?.agentId;
|
|
1275
|
+
const agent = await this._engine.getAgent({ id: agentId }).catch(() => null);
|
|
1276
|
+
if (!agent) {
|
|
1277
|
+
this._seen.delete(runId);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (!agent.providerDeviceId?.equals(this._deviceId))
|
|
1281
|
+
return;
|
|
1282
|
+
if (await this._isHandled(request))
|
|
1283
|
+
return;
|
|
1284
|
+
this._log?.info('runs', 'run claimed', { runId, chatId: request.chatId, agentId });
|
|
1285
|
+
const controller = new AbortController();
|
|
1286
|
+
const prior = this._chats.get(request.chatId) ?? Promise.resolve();
|
|
1287
|
+
const done = prior.then(() => this._execute(request, agent, controller));
|
|
1288
|
+
const link = done.catch(noop);
|
|
1289
|
+
this._chats.set(request.chatId, link);
|
|
1290
|
+
this._runs.set(runId, {
|
|
1291
|
+
controller,
|
|
1292
|
+
done,
|
|
1293
|
+
chatId: request.chatId,
|
|
1294
|
+
model: agent.modelName,
|
|
1295
|
+
executing: false
|
|
1296
|
+
});
|
|
1297
|
+
try {
|
|
1298
|
+
await done;
|
|
1299
|
+
}
|
|
1300
|
+
finally {
|
|
1301
|
+
this._runImageCacheNames.delete(runId);
|
|
1302
|
+
this._runs.delete(runId);
|
|
1303
|
+
this._yieldDownloads();
|
|
1304
|
+
if (this._chats.get(request.chatId) === link)
|
|
1305
|
+
this._chats.delete(request.chatId);
|
|
1306
|
+
this._reportCapabilities().catch(noop);
|
|
1307
|
+
}
|
|
1308
|
+
})();
|
|
1309
|
+
settle.catch((err) => this._log?.error('assistant', `run ${runId} failed`, err));
|
|
1310
|
+
}
|
|
1311
|
+
async _citeWebSources(transcript, runId, result) {
|
|
1312
|
+
const records = webSourcesFromToolResult(result);
|
|
1313
|
+
if (records.length === 0)
|
|
1314
|
+
return;
|
|
1315
|
+
try {
|
|
1316
|
+
await transcript.chunk({ type: 'sources', sources: { sources: records } });
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
this._log?.warn('assistant', 'web sources chunk skipped', { runId, err });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async _searchKnowledge(chatId, messages, attachmentIds, signal, preparing) {
|
|
1323
|
+
let lastUser;
|
|
1324
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1325
|
+
if (messages[index].role === 'user') {
|
|
1326
|
+
lastUser = messages[index];
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const query = lastUser?.content.trim();
|
|
1331
|
+
if (!lastUser || !query)
|
|
1332
|
+
return null;
|
|
1333
|
+
try {
|
|
1334
|
+
const search = await searchKnowledge(this._engine, {
|
|
1335
|
+
chatId,
|
|
1336
|
+
query,
|
|
1337
|
+
attachmentIds: attachmentIds.length > 0 ? [...attachmentIds] : undefined
|
|
1338
|
+
}, { signal, preparing });
|
|
1339
|
+
if (!search.enabled)
|
|
1340
|
+
return null;
|
|
1341
|
+
if (!search.hasKnowledge)
|
|
1342
|
+
return null;
|
|
1343
|
+
return { search, lastUser, hits: search.hits ?? [] };
|
|
1344
|
+
}
|
|
1345
|
+
catch (err) {
|
|
1346
|
+
if (err instanceof AbortedError)
|
|
1347
|
+
return null;
|
|
1348
|
+
this._log?.warn('assistant', 'knowledge search unavailable', err);
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async _isHandled(request) {
|
|
1353
|
+
const { chunks } = await this._engine.listChunks({
|
|
1354
|
+
chatId: request.chatId,
|
|
1355
|
+
type: 'run-status'
|
|
1356
|
+
});
|
|
1357
|
+
return (chunks ?? []).some((c) => c.runId === request.runId && TERMINAL_STATES.has(c.runStatus?.state ?? ''));
|
|
1358
|
+
}
|
|
1359
|
+
async _execute(request, agent, controller) {
|
|
1360
|
+
const { chatId } = request;
|
|
1361
|
+
const runId = request.runId;
|
|
1362
|
+
const deviceId = this._deviceId;
|
|
1363
|
+
if (this.suspended) {
|
|
1364
|
+
this._seen.delete(runId);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (await this._isHandled(request))
|
|
1368
|
+
return;
|
|
1369
|
+
const fresh = await this._engine.getAgent({ id: agent.id }).catch(() => null);
|
|
1370
|
+
if (!fresh || fresh.deletedAt || !fresh.providerDeviceId?.equals(deviceId))
|
|
1371
|
+
return;
|
|
1372
|
+
agent = fresh;
|
|
1373
|
+
await this._reconcileCompaction(chatId);
|
|
1374
|
+
if (request.parentId)
|
|
1375
|
+
await this._deleteChatKvCache(chatId, 'superseding run');
|
|
1376
|
+
const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
|
|
1377
|
+
if (model !== agent.modelName) {
|
|
1378
|
+
this._log?.warn('runs', 'model clamped to what this device can run', {
|
|
1379
|
+
runId,
|
|
1380
|
+
chatId,
|
|
1381
|
+
requested: agent.modelName,
|
|
1382
|
+
model
|
|
1383
|
+
});
|
|
1384
|
+
const inflight = this._runs.get(runId);
|
|
1385
|
+
if (inflight)
|
|
1386
|
+
this._runs.set(runId, { ...inflight, model });
|
|
1387
|
+
}
|
|
1388
|
+
const config = await this._engine.getConfig({}).catch(noop);
|
|
1389
|
+
const configuredAgent = withConfigDefaults(agent, config);
|
|
1390
|
+
this._log?.info('runs', 'run model resolved', {
|
|
1391
|
+
runId,
|
|
1392
|
+
model,
|
|
1393
|
+
agentModel: agent.modelName,
|
|
1394
|
+
warmedModel: this._warmedModelName
|
|
1395
|
+
});
|
|
1396
|
+
const transcript = new RunTranscript(this._engine, {
|
|
1397
|
+
chatId,
|
|
1398
|
+
runId,
|
|
1399
|
+
deviceId,
|
|
1400
|
+
model,
|
|
1401
|
+
log: this._log
|
|
1402
|
+
});
|
|
1403
|
+
await this._compactIfNearFull({
|
|
1404
|
+
chatId,
|
|
1405
|
+
transcript,
|
|
1406
|
+
trigger: 'before-run',
|
|
1407
|
+
signal: controller.signal
|
|
1408
|
+
});
|
|
1409
|
+
const toolConfigsPromise = this._loadToolConfigs(agent.id);
|
|
1410
|
+
const fsGrantsPromise = this._loadFsGrants(agent.id);
|
|
1411
|
+
const all = await listHistoryChunks(this._engine, chatId);
|
|
1412
|
+
const branches = historyBranches(all, runId);
|
|
1413
|
+
const currentRepair = branches.repairByRunId.get(runId);
|
|
1414
|
+
const repairParent = currentRepair
|
|
1415
|
+
? all.find((chunk) => chunk.id === currentRepair.parentId)
|
|
1416
|
+
: undefined;
|
|
1417
|
+
const acceptedRepairs = acceptedRepairCount(branches, runId);
|
|
1418
|
+
const history = all.filter((chunk) => Boolean(chunk.compaction) ||
|
|
1419
|
+
(chunk.runId !== runId &&
|
|
1420
|
+
inputAvailableToRun(chunk, request.seq) &&
|
|
1421
|
+
(!repairParent || chunk.seq <= repairParent.seq || chunk.runId === repairParent.runId) &&
|
|
1422
|
+
isPromptChunk(chunk, branches)));
|
|
1423
|
+
const { chunks: priorAsks } = await this._engine.listChunks({
|
|
1424
|
+
chatId,
|
|
1425
|
+
type: 'approval-request'
|
|
1426
|
+
});
|
|
1427
|
+
const { chunks: priorResults } = await this._engine.listChunks({ chatId, type: 'tool-result' });
|
|
1428
|
+
let ran = (priorResults ?? []).some((chunk) => chunk.runId === runId &&
|
|
1429
|
+
chunk.toolResult?.name !== SKILL_TOOL &&
|
|
1430
|
+
!isDeniedResult(chunk.toolResult?.result));
|
|
1431
|
+
const messages = historyFromChunks(history, staleToolRunIds(history, priorAsks ?? [], runId));
|
|
1432
|
+
if (currentRepair)
|
|
1433
|
+
messages.push({ role: 'user', content: currentRepair.promptText });
|
|
1434
|
+
const diagramRepair = currentRepair !== undefined;
|
|
1435
|
+
const runAgent = diagramRepair
|
|
1436
|
+
? {
|
|
1437
|
+
...configuredAgent,
|
|
1438
|
+
toolsScope: 'selected',
|
|
1439
|
+
enabledTools: [],
|
|
1440
|
+
skillsScope: 'selected',
|
|
1441
|
+
enabledSkills: []
|
|
1442
|
+
}
|
|
1443
|
+
: configuredAgent;
|
|
1444
|
+
const seesImages = this._harness.supportsImagesFor
|
|
1445
|
+
? this._harness.supportsImagesFor(model)
|
|
1446
|
+
: (this._harness.supportsImages ?? this._info.supportsImages) !== false;
|
|
1447
|
+
let unreadableImages = new Map();
|
|
1448
|
+
if (!seesImages) {
|
|
1449
|
+
const dropped = messages.reduce((count, turn) => count + (turn.images?.length ?? 0), 0);
|
|
1450
|
+
if (dropped && (this._harness.supportsImages ?? this._info.supportsImages)) {
|
|
1451
|
+
this._log?.warn('assistant', `no projection pairing for ${model} — dropped ${dropped} image(s)`);
|
|
1452
|
+
}
|
|
1453
|
+
for (const turn of messages) {
|
|
1454
|
+
if (turn.images?.length) {
|
|
1455
|
+
const names = turn.images.map((image) => image.fileName).join(', ');
|
|
1456
|
+
turn.content += `\n[Attached image${turn.images.length > 1 ? 's' : ''}: ${names} — not visible to this model]`;
|
|
1457
|
+
}
|
|
1458
|
+
delete turn.images;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
unreadableImages = await this._hydrateImages(messages, controller.signal, runId);
|
|
1463
|
+
}
|
|
1464
|
+
const settledWatch = this._watchSettled(chatId, runId, controller);
|
|
1465
|
+
const ownershipWatch = this._watchOwnership(agent.id, controller);
|
|
1466
|
+
let readinessError = null;
|
|
1467
|
+
try {
|
|
1468
|
+
await this._downloads?.waitUntilCached(model, controller.signal);
|
|
1469
|
+
}
|
|
1470
|
+
catch (err) {
|
|
1471
|
+
readinessError = err instanceof Error ? err : new Error(String(err));
|
|
1472
|
+
}
|
|
1473
|
+
if (controller.signal.aborted) {
|
|
1474
|
+
try {
|
|
1475
|
+
const outcome = this._abortOutcome(runId, controller.signal.reason);
|
|
1476
|
+
if (outcome.kind === 'replay')
|
|
1477
|
+
this._seen.delete(runId);
|
|
1478
|
+
if (outcome.kind === 'failed') {
|
|
1479
|
+
await transcript.status({ state: 'failed', reason: outcome.reason });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
finally {
|
|
1483
|
+
transcript.close();
|
|
1484
|
+
ownershipWatch.stop();
|
|
1485
|
+
settledWatch.stop();
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (readinessError) {
|
|
1490
|
+
try {
|
|
1491
|
+
await transcript.status({ state: 'failed', reason: runFailureNotice(readinessError) });
|
|
1492
|
+
}
|
|
1493
|
+
finally {
|
|
1494
|
+
transcript.close();
|
|
1495
|
+
ownershipWatch.stop();
|
|
1496
|
+
settledWatch.stop();
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const inflight = this._runs.get(runId);
|
|
1501
|
+
if (inflight)
|
|
1502
|
+
this._runs.set(runId, { ...inflight, executing: true });
|
|
1503
|
+
this._yieldDownloads();
|
|
1504
|
+
const busyChatId = this._modelHolder(runId, chatId, model);
|
|
1505
|
+
const executing = await transcript.status(busyChatId
|
|
1506
|
+
? { state: 'executing', progress: { phase: 'queued', busyChatId } }
|
|
1507
|
+
: { state: 'executing' });
|
|
1508
|
+
const runStatusId = executing.id;
|
|
1509
|
+
const approvals = this._createApprove(chatId, runId, deviceId, agent.id, () => ran);
|
|
1510
|
+
const modelWatch = this._watchModelProgress((runStatus) => transcript.progress(runStatusId, runStatus), busyChatId !== null);
|
|
1511
|
+
let metrics;
|
|
1512
|
+
const sourceTagFilter = createRagSourceTagFilter();
|
|
1513
|
+
const retrievedRecords = [];
|
|
1514
|
+
let answerText = '';
|
|
1515
|
+
let ragInjected = false;
|
|
1516
|
+
const flushTranscript = async () => {
|
|
1517
|
+
const tail = sourceTagFilter.flush();
|
|
1518
|
+
if (tail) {
|
|
1519
|
+
answerText += tail;
|
|
1520
|
+
await transcript.text('content', tail);
|
|
1521
|
+
}
|
|
1522
|
+
await transcript.flush();
|
|
1523
|
+
};
|
|
1524
|
+
let titleOutcome = null;
|
|
1525
|
+
try {
|
|
1526
|
+
const attachTurnIds = attachTurnDocumentIds(history);
|
|
1527
|
+
if (attachTurnIds.length > 0)
|
|
1528
|
+
backfillDocumentOnlyTurn(messages);
|
|
1529
|
+
const knowledge = !diagramRepair &&
|
|
1530
|
+
(toolsetEnabled('knowledge', runAgent.disabledToolsets) || attachTurnIds.length > 0)
|
|
1531
|
+
? await this._searchKnowledge(chatId, messages, attachTurnIds, controller.signal, modelWatch.documents)
|
|
1532
|
+
: null;
|
|
1533
|
+
if (controller.signal.aborted) {
|
|
1534
|
+
const outcome = this._abortOutcome(runId, controller.signal.reason);
|
|
1535
|
+
if (outcome.kind === 'replay') {
|
|
1536
|
+
this._seen.delete(runId);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (outcome.kind === 'handoff')
|
|
1540
|
+
return;
|
|
1541
|
+
await transcript.status({ state: 'failed', reason: outcome.reason, metrics });
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
let documentsInjected = false;
|
|
1545
|
+
let sourcesWritten = false;
|
|
1546
|
+
if (knowledge) {
|
|
1547
|
+
try {
|
|
1548
|
+
if (knowledge.search.errorMessage) {
|
|
1549
|
+
this._log?.warn('assistant', 'knowledge search failed', {
|
|
1550
|
+
error: knowledge.search.errorMessage
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
const fullTextOnly = knowledge.hits.length > 0 && knowledge.hits.every(isFullTextHit);
|
|
1554
|
+
if (fullTextOnly) {
|
|
1555
|
+
knowledge.lastUser.content = `${formatFullDocumentContext(knowledge.hits)}\n\nThe user's message:\n${knowledge.lastUser.content}`;
|
|
1556
|
+
documentsInjected = true;
|
|
1557
|
+
if (knowledge.hits.some(isTruncatedFullTextHit)) {
|
|
1558
|
+
await transcript.chunk({
|
|
1559
|
+
type: 'sources',
|
|
1560
|
+
sources: { sources: [], notice: RAG_TRUNCATED_NOTICE }
|
|
1561
|
+
});
|
|
1562
|
+
sourcesWritten = true;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
else {
|
|
1566
|
+
const records = sourcesFromHits(knowledge.hits);
|
|
1567
|
+
const notice = chooseRagNotice({
|
|
1568
|
+
errorMessage: knowledge.search.errorMessage,
|
|
1569
|
+
hitCount: records.length,
|
|
1570
|
+
searchedCount: knowledge.search.searchedCount ?? 0,
|
|
1571
|
+
indexEmpty: Boolean(knowledge.search.indexEmpty),
|
|
1572
|
+
hasKnowledge: Boolean(knowledge.search.hasKnowledge),
|
|
1573
|
+
pendingReferenced: Boolean(knowledge.search.pendingReferenced),
|
|
1574
|
+
failedFiles: knowledge.search.failedFiles ?? [],
|
|
1575
|
+
pendingFiles: knowledge.search.pendingFiles ?? []
|
|
1576
|
+
});
|
|
1577
|
+
sourceTagFilter.add(records.map((record) => record.title));
|
|
1578
|
+
retrievedRecords.push(...records.map((record, index) => ({
|
|
1579
|
+
...record,
|
|
1580
|
+
excerpt: knowledge.hits[index]?.content ?? record.excerpt
|
|
1581
|
+
})));
|
|
1582
|
+
await transcript.chunk({
|
|
1583
|
+
type: 'sources',
|
|
1584
|
+
sources: { sources: records, notice: notice ?? undefined }
|
|
1585
|
+
});
|
|
1586
|
+
sourcesWritten = true;
|
|
1587
|
+
if (records.length > 0) {
|
|
1588
|
+
knowledge.lastUser.content = `${formatRagContext(knowledge.hits, knowledge.search.availableFiles ?? [])}\n\nThe user's message:\n${knowledge.lastUser.content}\n\n${RAG_CITATION_LINE}`;
|
|
1589
|
+
ragInjected = true;
|
|
1590
|
+
documentsInjected = true;
|
|
1591
|
+
}
|
|
1592
|
+
else if (knowledge.search.availableFiles?.length) {
|
|
1593
|
+
knowledge.lastUser.content += `\n\n[Files indexed and available now: ${knowledge.search.availableFiles.join(', ')}. No relevant excerpts were retrieved for this question. Their current availability supersedes earlier indexing notices.]`;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (knowledge.search.pendingFiles?.length) {
|
|
1597
|
+
knowledge.lastUser.content += `\n\n[Files not yet available for this reply: ${knowledge.search.pendingFiles.join(', ')}.]`;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
catch (err) {
|
|
1601
|
+
this._log?.warn('assistant', 'knowledge injection skipped', err);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const hasAudio = messages.some((turn) => turn.audioAttachments?.length);
|
|
1605
|
+
if (hasAudio)
|
|
1606
|
+
modelWatch.transcribe(true);
|
|
1607
|
+
await this._hydrateAudio(messages, controller.signal);
|
|
1608
|
+
if (hasAudio)
|
|
1609
|
+
modelWatch.transcribe(false);
|
|
1610
|
+
backfillImageOnlyTurns(messages);
|
|
1611
|
+
annotateDocumentAttachments(messages);
|
|
1612
|
+
if (!diagramRepair && attachTurnIds.length > 0 && !documentsInjected) {
|
|
1613
|
+
const reason = documentUnreadReason({
|
|
1614
|
+
knowledgeOn: toolsetEnabled('knowledge', runAgent.disabledToolsets),
|
|
1615
|
+
failedFiles: knowledge?.search?.failedFiles,
|
|
1616
|
+
attachedCount: attachTurnIds.length
|
|
1617
|
+
});
|
|
1618
|
+
const lastUser = messages.findLast((turn) => turn.role === 'user');
|
|
1619
|
+
if (lastUser) {
|
|
1620
|
+
lastUser.content = `${lastUser.content}\n${documentUnreadNotice(reason, attachTurnIds.length > 1)}`;
|
|
1621
|
+
}
|
|
1622
|
+
if (!sourcesWritten) {
|
|
1623
|
+
try {
|
|
1624
|
+
await transcript.chunk({
|
|
1625
|
+
type: 'sources',
|
|
1626
|
+
sources: {
|
|
1627
|
+
sources: [],
|
|
1628
|
+
notice: reason === 'knowledge-off' ? RAG_KNOWLEDGE_OFF_NOTICE : RAG_UNAVAILABLE_NOTICE
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
this._log?.warn('assistant', 'unread-document notice skipped', err);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
annotateUnreadableImages(unreadableImages);
|
|
1638
|
+
const toolConfigs = await toolConfigsPromise;
|
|
1639
|
+
const { roots, write } = await fsGrantsPromise;
|
|
1640
|
+
const { grantedDirs, grantedDrives, driveOp } = await resolveFsGrants(roots, this._drives, (key, err) => this._log?.warn('assistant', 'drive grant dropped', {
|
|
1641
|
+
key,
|
|
1642
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1643
|
+
}));
|
|
1644
|
+
if (await this._isHandled(request))
|
|
1645
|
+
return;
|
|
1646
|
+
const cacheKey = diagramRepair ? undefined : chatRepairKvCacheKey(chatId, acceptedRepairs);
|
|
1647
|
+
this._log?.info('runs', 'completion requested', {
|
|
1648
|
+
runId,
|
|
1649
|
+
model,
|
|
1650
|
+
cacheKey,
|
|
1651
|
+
turns: messages.length
|
|
1652
|
+
});
|
|
1653
|
+
const events = this._harness.completion({
|
|
1654
|
+
model,
|
|
1655
|
+
agent: runAgent,
|
|
1656
|
+
messages,
|
|
1657
|
+
cacheKey,
|
|
1658
|
+
thinkingDefault: config ? (config.thinkingEnabled ? 'enabled' : 'disabled') : undefined,
|
|
1659
|
+
toolConfigs,
|
|
1660
|
+
grantedDirs,
|
|
1661
|
+
grantedDrives,
|
|
1662
|
+
driveOp,
|
|
1663
|
+
grantedWriteDirs: write,
|
|
1664
|
+
approve: approvals.approve,
|
|
1665
|
+
attach: (input) => this._uploadGenerated(chatId, runId, deviceId, input),
|
|
1666
|
+
fetchAttachment: (query) => this._fetchAttachment(chatId, query),
|
|
1667
|
+
ragSearch: async (input) => {
|
|
1668
|
+
const found = await raceAbort(this._engine.ragSearch({
|
|
1669
|
+
chatId,
|
|
1670
|
+
query: input.query,
|
|
1671
|
+
mode: 'tool',
|
|
1672
|
+
limit: input.limit
|
|
1673
|
+
}), controller.signal);
|
|
1674
|
+
const hits = found.hits ?? [];
|
|
1675
|
+
const records = sourcesFromHits(hits);
|
|
1676
|
+
if (records.length > 0) {
|
|
1677
|
+
try {
|
|
1678
|
+
sourceTagFilter.add(records.map((record) => record.title));
|
|
1679
|
+
retrievedRecords.push(...records.map((record, index) => ({
|
|
1680
|
+
...record,
|
|
1681
|
+
excerpt: hits[index]?.content ?? record.excerpt
|
|
1682
|
+
})));
|
|
1683
|
+
await transcript.chunk({
|
|
1684
|
+
type: 'sources',
|
|
1685
|
+
sources: { sources: records }
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
catch (err) {
|
|
1689
|
+
this._log?.warn('assistant', 'rag tool sources chunk skipped', { runId, err });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
enabled: found.enabled === true,
|
|
1694
|
+
indexEmpty: found.indexEmpty === true,
|
|
1695
|
+
hasKnowledge: found.hasKnowledge === true,
|
|
1696
|
+
pendingReferenced: found.pendingReferenced === true,
|
|
1697
|
+
errorMessage: found.errorMessage ?? undefined,
|
|
1698
|
+
hits
|
|
1699
|
+
};
|
|
1700
|
+
},
|
|
1701
|
+
progress: (update) => modelWatch.image(update),
|
|
1702
|
+
generationParams: diagramRepair ? { reasoning_budget: 0 } : undefined,
|
|
1703
|
+
signal: controller.signal
|
|
1704
|
+
});
|
|
1705
|
+
let harnessEvents = 0;
|
|
1706
|
+
for await (const event of events) {
|
|
1707
|
+
harnessEvents++;
|
|
1708
|
+
modelWatch.clear();
|
|
1709
|
+
if (event.type === 'content' || event.type === 'thinking') {
|
|
1710
|
+
const text = event.type === 'content'
|
|
1711
|
+
? sourceTagFilter.push(event.text)
|
|
1712
|
+
: event.text;
|
|
1713
|
+
if (text) {
|
|
1714
|
+
if (event.type === 'content')
|
|
1715
|
+
answerText += text;
|
|
1716
|
+
await transcript.text(event.type, text);
|
|
1717
|
+
}
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (event.type === 'metrics') {
|
|
1721
|
+
const { type, ...rest } = event;
|
|
1722
|
+
metrics = rest;
|
|
1723
|
+
this._log?.info('runs', 'run metrics', { runId, ...runMetricsSummary(rest) });
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
await flushTranscript();
|
|
1727
|
+
if (event.type === 'error') {
|
|
1728
|
+
const folded = isContextOverflowFailure(event.message) &&
|
|
1729
|
+
(await this._runCompaction({
|
|
1730
|
+
chatId,
|
|
1731
|
+
transcript,
|
|
1732
|
+
trigger: 'overflow',
|
|
1733
|
+
signal: controller.signal
|
|
1734
|
+
}));
|
|
1735
|
+
await transcript.status({
|
|
1736
|
+
state: 'failed',
|
|
1737
|
+
reason: runFailureNotice(event.message),
|
|
1738
|
+
metrics
|
|
1739
|
+
});
|
|
1740
|
+
if (folded) {
|
|
1741
|
+
await this._engine
|
|
1742
|
+
.runAgent({ chatId, agentId: agent.id, parentRunId: runId })
|
|
1743
|
+
.catch((err) => this._log?.warn('runs', 'compaction retry not queued', { runId, err }));
|
|
1744
|
+
}
|
|
1745
|
+
titleOutcome = 'ended';
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (event.type === 'aborted') {
|
|
1749
|
+
const outcome = this._abortOutcome(runId, controller.signal.reason);
|
|
1750
|
+
if (outcome.kind === 'replay') {
|
|
1751
|
+
this._seen.delete(runId);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (outcome.kind === 'handoff')
|
|
1755
|
+
return;
|
|
1756
|
+
await transcript.status({ state: 'failed', reason: outcome.reason, metrics });
|
|
1757
|
+
titleOutcome = 'ended';
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (event.type === 'tool-call') {
|
|
1761
|
+
this._log?.info('runs', 'tool call', { runId, name: event.name });
|
|
1762
|
+
await transcript.chunk({
|
|
1763
|
+
type: 'tool-call',
|
|
1764
|
+
toolCall: {
|
|
1765
|
+
name: event.name,
|
|
1766
|
+
arguments: event.args
|
|
1767
|
+
},
|
|
1768
|
+
effect: event.effect === 'read' ? 'read' : 'write',
|
|
1769
|
+
display: event.display,
|
|
1770
|
+
rawCall: typeof event.rawCall === 'string' ? event.rawCall : undefined
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
else if (event.type === 'tool-result') {
|
|
1774
|
+
if (isDeniedResult(event.result)) {
|
|
1775
|
+
this._log?.info('approvals', 'tool ran denied', { runId, tool: event.name });
|
|
1776
|
+
}
|
|
1777
|
+
else if (event.name !== SKILL_TOOL) {
|
|
1778
|
+
ran = true;
|
|
1779
|
+
}
|
|
1780
|
+
if (WEB_TOOLS.includes(String(event.name))) {
|
|
1781
|
+
await this._citeWebSources(transcript, runId, event.result);
|
|
1782
|
+
}
|
|
1783
|
+
await transcript.chunk({
|
|
1784
|
+
type: 'tool-result',
|
|
1785
|
+
toolResult: { name: event.name, result: withoutDataChannel(event.result) }
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
else if (event.type === 'tool-activity') {
|
|
1789
|
+
void this._engine
|
|
1790
|
+
.recordToolActivity({
|
|
1791
|
+
agentId: runAgent.id,
|
|
1792
|
+
toolName: event.name,
|
|
1793
|
+
decision: event.decision,
|
|
1794
|
+
effect: event.effect,
|
|
1795
|
+
arguments: event.args,
|
|
1796
|
+
resource: event.resource ?? undefined,
|
|
1797
|
+
chatId,
|
|
1798
|
+
runId,
|
|
1799
|
+
at: Date.now()
|
|
1800
|
+
})
|
|
1801
|
+
.catch((err) => this._log?.warn('runs', 'tool activity record failed', { runId, err }));
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
this._log?.warn('assistant', 'unknown harness event dropped', { type: event.type });
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
this._log?.info('runs', 'harness stream ended', { runId, events: harnessEvents });
|
|
1808
|
+
if (await settledWithin(flushTranscript(), FINAL_FLUSH_MS)) {
|
|
1809
|
+
this._log?.info('runs', 'final flush done', { runId });
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
this._log?.warn('runs', 'completing over a stranded flush', {
|
|
1813
|
+
runId,
|
|
1814
|
+
pending: transcript.pending()
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
const citedTitles = ragInjected ? citedByPhraseOverlap(answerText, retrievedRecords) : null;
|
|
1818
|
+
const foldedNow = metrics
|
|
1819
|
+
? await this._compactIfNearFull({
|
|
1820
|
+
chatId,
|
|
1821
|
+
transcript,
|
|
1822
|
+
trigger: 'turn-filled-window',
|
|
1823
|
+
metrics: metrics,
|
|
1824
|
+
signal: controller.signal
|
|
1825
|
+
})
|
|
1826
|
+
: false;
|
|
1827
|
+
const settled = foldedNow ? withoutOccupancy(metrics) : metrics;
|
|
1828
|
+
await transcript.status({
|
|
1829
|
+
state: 'completed',
|
|
1830
|
+
metrics: citedTitles ? { ...settled, citedTitles } : settled
|
|
1831
|
+
});
|
|
1832
|
+
titleOutcome = 'completed';
|
|
1833
|
+
}
|
|
1834
|
+
catch (err) {
|
|
1835
|
+
const outcome = controller.signal.aborted
|
|
1836
|
+
? this._abortOutcome(runId, controller.signal.reason)
|
|
1837
|
+
: null;
|
|
1838
|
+
if (outcome?.kind === 'replay') {
|
|
1839
|
+
this._seen.delete(runId);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (outcome?.kind === 'handoff')
|
|
1843
|
+
return;
|
|
1844
|
+
await flushTranscript();
|
|
1845
|
+
await transcript.status({ state: 'failed', reason: runFailureNotice(err), metrics });
|
|
1846
|
+
titleOutcome = 'ended';
|
|
1847
|
+
}
|
|
1848
|
+
finally {
|
|
1849
|
+
transcript.close();
|
|
1850
|
+
modelWatch.stop();
|
|
1851
|
+
ownershipWatch.stop();
|
|
1852
|
+
await approvals.settle({ preservePending: isLifecycleTeardown(controller.signal.reason) });
|
|
1853
|
+
settledWatch.stop();
|
|
1854
|
+
if (titleOutcome && !diagramRepair) {
|
|
1855
|
+
void this._autoTitle(chatId, messages, model, runAgent, titleOutcome === 'completed');
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async _loadToolConfigs(agentId) {
|
|
1860
|
+
const { configs } = await this._engine.listToolConfigs({ agentId }).catch((err) => {
|
|
1861
|
+
this._log?.warn('assistant', 'tool configs read failed', err);
|
|
1862
|
+
return { configs: [] };
|
|
1863
|
+
});
|
|
1864
|
+
const map = {};
|
|
1865
|
+
for (const row of configs ?? []) {
|
|
1866
|
+
if (row?.toolName && row.settings && typeof row.settings === 'object') {
|
|
1867
|
+
map[row.toolName] = row.settings;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return map;
|
|
1871
|
+
}
|
|
1872
|
+
async _loadFsGrants(agentId) {
|
|
1873
|
+
const [roots, write] = await Promise.all([
|
|
1874
|
+
this._readRootGrants(fsGrantKey(agentId)),
|
|
1875
|
+
this._readWriteGrants(fsWriteGrantKey(agentId))
|
|
1876
|
+
]);
|
|
1877
|
+
return { roots, write };
|
|
1878
|
+
}
|
|
1879
|
+
async _readRootGrants(key) {
|
|
1880
|
+
const { value } = await this._engine.configGet({ key }).catch((err) => {
|
|
1881
|
+
this._log?.warn('assistant', 'fs grants read failed', { key, err });
|
|
1882
|
+
return { value: null };
|
|
1883
|
+
});
|
|
1884
|
+
return fsGrantsFrom(value);
|
|
1885
|
+
}
|
|
1886
|
+
async _readWriteGrants(key) {
|
|
1887
|
+
const { value } = await this._engine.configGet({ key }).catch((err) => {
|
|
1888
|
+
this._log?.warn('assistant', 'fs write grants read failed', { key, err });
|
|
1889
|
+
return { value: null };
|
|
1890
|
+
});
|
|
1891
|
+
return grantedDirsFrom(value);
|
|
1892
|
+
}
|
|
1893
|
+
async _fetchAttachment(chatId, { attachmentId, signal, meta } = {}) {
|
|
1894
|
+
if (!attachmentId) {
|
|
1895
|
+
const { chunks } = await raceAbort(this._engine.listChunks({ chatId, type: 'attachment' }), signal);
|
|
1896
|
+
attachmentId = (chunks ?? [])
|
|
1897
|
+
.map((chunk) => chunk.attachment)
|
|
1898
|
+
.filter((file) => file && !file.deletedAt && IMAGE_MIMES.has(file.mimeType.toLowerCase()))
|
|
1899
|
+
.at(-1)?.id;
|
|
1900
|
+
}
|
|
1901
|
+
if (!attachmentId)
|
|
1902
|
+
return null;
|
|
1903
|
+
const stream = this._engine.fileRead({ chatId, fileId: attachmentId, metadataOnly: meta });
|
|
1904
|
+
const abort = () => stream.destroy();
|
|
1905
|
+
const pieces = [];
|
|
1906
|
+
let head = null;
|
|
1907
|
+
stream.on('error', noop);
|
|
1908
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
1909
|
+
try {
|
|
1910
|
+
if (signal?.aborted)
|
|
1911
|
+
throw new AbortedError();
|
|
1912
|
+
for await (const frame of stream) {
|
|
1913
|
+
if (frame.metadata) {
|
|
1914
|
+
const { id, fileName, mimeType, byteLength } = frame.metadata;
|
|
1915
|
+
head = { id, fileName, mimeType, byteLength };
|
|
1916
|
+
}
|
|
1917
|
+
if (frame.data)
|
|
1918
|
+
pieces.push(frame.data);
|
|
1919
|
+
}
|
|
1920
|
+
if (signal?.aborted)
|
|
1921
|
+
throw new AbortedError();
|
|
1922
|
+
return head && (meta ? head : { ...head, bytes: Buffer.concat(pieces) });
|
|
1923
|
+
}
|
|
1924
|
+
finally {
|
|
1925
|
+
signal?.removeEventListener('abort', abort);
|
|
1926
|
+
stream.destroy();
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async _readBlob(url, signal) {
|
|
1930
|
+
if (!url || signal?.aborted)
|
|
1931
|
+
return null;
|
|
1932
|
+
const pending = defaultFetch(url).catch(() => null);
|
|
1933
|
+
try {
|
|
1934
|
+
const response = await raceInactivity(pending, signal, this._blobReadTimeoutMs);
|
|
1935
|
+
const bytes = response?.ok
|
|
1936
|
+
? await drainBody(response.body, signal, this._blobReadTimeoutMs)
|
|
1937
|
+
: null;
|
|
1938
|
+
return bytes?.length ? bytes : null;
|
|
1939
|
+
}
|
|
1940
|
+
catch {
|
|
1941
|
+
pending.then((response) => response?.body?.cancel(), noop);
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
async _uploadGenerated(chatId, runId, deviceId, { fileName, mimeType, bytes, raw }) {
|
|
1946
|
+
const duplex = this._engine.uploadAttachment();
|
|
1947
|
+
duplex.write({ chatId, runId, originDeviceId: deviceId, fileName, mimeType, raw });
|
|
1948
|
+
const buf = Buffer.isBuffer(bytes)
|
|
1949
|
+
? bytes
|
|
1950
|
+
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1951
|
+
for (let i = 0; i < buf.length; i += UPLOAD_FRAME_BYTES) {
|
|
1952
|
+
duplex.write({ data: buf.subarray(i, i + UPLOAD_FRAME_BYTES) });
|
|
1953
|
+
}
|
|
1954
|
+
duplex.end();
|
|
1955
|
+
let attachment = null;
|
|
1956
|
+
for await (const frame of duplex) {
|
|
1957
|
+
if (frame.attachment)
|
|
1958
|
+
attachment = frame.attachment;
|
|
1959
|
+
}
|
|
1960
|
+
if (!attachment)
|
|
1961
|
+
throw new Error('attachment upload ended without a record');
|
|
1962
|
+
return attachment;
|
|
1963
|
+
}
|
|
1964
|
+
async _hydrateAudio(messages, signal) {
|
|
1965
|
+
const turns = messages.filter((turn) => turn.audioAttachments?.length);
|
|
1966
|
+
if (!turns.length)
|
|
1967
|
+
return;
|
|
1968
|
+
for (const turn of turns) {
|
|
1969
|
+
for (const ref of turn.audioAttachments ?? []) {
|
|
1970
|
+
if (signal.aborted)
|
|
1971
|
+
return;
|
|
1972
|
+
const block = await this._transcribeAttachment(ref, signal);
|
|
1973
|
+
turn.content = turn.content.trim() ? `${turn.content}\n\n${block}` : block;
|
|
1974
|
+
}
|
|
1975
|
+
delete turn.audioAttachments;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
async _transcribeAttachment(ref, signal) {
|
|
1979
|
+
const label = `Attached audio "${ref.fileName}"`;
|
|
1980
|
+
const cached = this._audioTranscripts.get(ref.blobId);
|
|
1981
|
+
if (cached !== undefined) {
|
|
1982
|
+
return cached ? audioTranscriptBlock(ref.fileName, cached) : silentAudioNotice(ref.fileName);
|
|
1983
|
+
}
|
|
1984
|
+
if (!this._harness.transcribe) {
|
|
1985
|
+
return unheardAudioNotice(label, 'transcription is unavailable on this device', {
|
|
1986
|
+
retryable: false
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
try {
|
|
1990
|
+
const bytes = await this._readBlob(ref.url, signal);
|
|
1991
|
+
if (!bytes) {
|
|
1992
|
+
return unheardAudioNotice(label, 'its content has not reached this device — the upload may still be syncing');
|
|
1993
|
+
}
|
|
1994
|
+
const model = mediaModelNameFor('transcription', await this._persistedProfile());
|
|
1995
|
+
await this.ensureMediaModelUnpaused('transcription', model);
|
|
1996
|
+
await this._downloads?.waitUntilCached(model, signal);
|
|
1997
|
+
const text = (await this._harness.transcribe({ model: '', audio: bytes, signal })).trim();
|
|
1998
|
+
this._audioTranscripts.set(ref.blobId, text);
|
|
1999
|
+
if (!text)
|
|
2000
|
+
return silentAudioNotice(ref.fileName);
|
|
2001
|
+
return audioTranscriptBlock(ref.fileName, text);
|
|
2002
|
+
}
|
|
2003
|
+
catch (err) {
|
|
2004
|
+
if (signal.aborted)
|
|
2005
|
+
throw err;
|
|
2006
|
+
this._log?.warn('assistant', `audio transcription failed — ${ref.fileName}`, err);
|
|
2007
|
+
return unheardAudioNotice(label, 'transcription failed on this device');
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async _hydrateImages(messages, signal, runId) {
|
|
2011
|
+
const unreadable = new Map();
|
|
2012
|
+
const turns = messages.filter((turn) => turn.images?.length);
|
|
2013
|
+
if (!turns.length)
|
|
2014
|
+
return unreadable;
|
|
2015
|
+
const inUse = new Set(turns.flatMap((turn) => (turn.images ?? []).map(imageCacheName)));
|
|
2016
|
+
this._runImageCacheNames.set(runId, inUse);
|
|
2017
|
+
const dir = this._imageCacheDir;
|
|
2018
|
+
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
2019
|
+
for (const turn of turns) {
|
|
2020
|
+
const dropped = [];
|
|
2021
|
+
for (const image of turn.images ?? []) {
|
|
2022
|
+
try {
|
|
2023
|
+
const file = await this._hydrateImage(dir, image, signal);
|
|
2024
|
+
turn.attachments = [...(turn.attachments ?? []), { path: file }];
|
|
2025
|
+
}
|
|
2026
|
+
catch (err) {
|
|
2027
|
+
if (signal.aborted)
|
|
2028
|
+
throw err;
|
|
2029
|
+
this._log?.error('assistant', `image hydration failed — skipped ${image.fileName}`, err);
|
|
2030
|
+
dropped.push(image.fileName);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (dropped.length)
|
|
2034
|
+
unreadable.set(turn, dropped);
|
|
2035
|
+
delete turn.images;
|
|
2036
|
+
}
|
|
2037
|
+
return unreadable;
|
|
2038
|
+
}
|
|
2039
|
+
async _hydrateImage(dir, image, signal) {
|
|
2040
|
+
const file = path.join(dir, imageCacheName(image));
|
|
2041
|
+
const cached = await fs.promises.stat(file).catch(() => null);
|
|
2042
|
+
if (!cached?.size) {
|
|
2043
|
+
const raw = await this._readBlob(image.url, signal);
|
|
2044
|
+
if (signal.aborted)
|
|
2045
|
+
throw new Error('hydration aborted');
|
|
2046
|
+
if (!raw)
|
|
2047
|
+
throw new Error(`empty download for ${image.fileName}`);
|
|
2048
|
+
const bytes = await normalizeImageForModel(raw);
|
|
2049
|
+
const staging = `${file}.partial-${crypto.randomBytes(4).toString('hex')}`;
|
|
2050
|
+
await fs.promises.writeFile(staging, bytes, { mode: 0o600 });
|
|
2051
|
+
await fs.promises.rename(staging, file);
|
|
2052
|
+
}
|
|
2053
|
+
else {
|
|
2054
|
+
const now = new Date();
|
|
2055
|
+
await fs.promises.utimes(file, now, now).catch(() => null);
|
|
2056
|
+
}
|
|
2057
|
+
return file;
|
|
2058
|
+
}
|
|
2059
|
+
async _compactIfNearFull(params) {
|
|
2060
|
+
const { chatId, transcript, trigger, signal } = params;
|
|
2061
|
+
const metrics = params.metrics ?? (await this._lastRunMetrics(chatId));
|
|
2062
|
+
if (!nearContextLimit(metrics.cacheTokens, metrics.ctxSize))
|
|
2063
|
+
return false;
|
|
2064
|
+
if (!canCompact(metrics.ctxSize)) {
|
|
2065
|
+
this._log?.debug('runs', 'chat near its window but under the compaction floor', {
|
|
2066
|
+
chatId,
|
|
2067
|
+
ctxSize: metrics.ctxSize
|
|
2068
|
+
});
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
return this._runCompaction({ chatId, transcript, trigger, signal, occupancy: metrics });
|
|
2072
|
+
}
|
|
2073
|
+
async _runCompaction(params) {
|
|
2074
|
+
const { chatId, transcript, trigger, signal } = params;
|
|
2075
|
+
let statusId = null;
|
|
2076
|
+
const started = Date.now();
|
|
2077
|
+
try {
|
|
2078
|
+
const result = await this.compactChat(chatId, {
|
|
2079
|
+
onFold: async () => {
|
|
2080
|
+
const status = await transcript.status({
|
|
2081
|
+
state: 'executing',
|
|
2082
|
+
progress: { phase: 'compacting' }
|
|
2083
|
+
});
|
|
2084
|
+
statusId = status.id;
|
|
2085
|
+
},
|
|
2086
|
+
...(signal ? { signal } : {})
|
|
2087
|
+
});
|
|
2088
|
+
this._log?.info('runs', 'compaction', {
|
|
2089
|
+
chatId,
|
|
2090
|
+
trigger,
|
|
2091
|
+
compacted: result.compacted,
|
|
2092
|
+
reason: result.reason ?? null,
|
|
2093
|
+
foldedTurns: result.foldedTurns ?? 0,
|
|
2094
|
+
cacheTokens: params.occupancy?.cacheTokens ?? null,
|
|
2095
|
+
ctxSize: params.occupancy?.ctxSize ?? null,
|
|
2096
|
+
announced: statusId !== null,
|
|
2097
|
+
ms: Date.now() - started
|
|
2098
|
+
});
|
|
2099
|
+
return result.compacted;
|
|
2100
|
+
}
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
this._log?.warn('runs', 'compaction failed', { chatId, trigger, err });
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
finally {
|
|
2106
|
+
if (trigger === 'before-run' && statusId) {
|
|
2107
|
+
await transcript.progress(statusId, { state: 'executing' }).catch(noop);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async _lastRunMetrics(chatId) {
|
|
2112
|
+
const deviceId = this._deviceId;
|
|
2113
|
+
const { chunks } = await this._engine
|
|
2114
|
+
.listChunks({ chatId, type: 'run-status' })
|
|
2115
|
+
.catch(() => ({ chunks: [] }));
|
|
2116
|
+
const statuses = chunks ?? [];
|
|
2117
|
+
for (let index = statuses.length - 1; index >= 0; index--) {
|
|
2118
|
+
const chunk = statuses[index];
|
|
2119
|
+
const metrics = chunk.runStatus?.metrics;
|
|
2120
|
+
if (chunk.runStatus?.state !== 'completed' || !metrics)
|
|
2121
|
+
continue;
|
|
2122
|
+
if (!chunk.originDeviceId?.equals(deviceId))
|
|
2123
|
+
return {};
|
|
2124
|
+
const parsed = metrics;
|
|
2125
|
+
return { cacheTokens: parsed.cacheTokens, ctxSize: parsed.ctxSize };
|
|
2126
|
+
}
|
|
2127
|
+
return {};
|
|
2128
|
+
}
|
|
2129
|
+
async compactChat(chatId, options = {}) {
|
|
2130
|
+
if (!this.opened)
|
|
2131
|
+
await this.ready();
|
|
2132
|
+
const deviceId = this._deviceId;
|
|
2133
|
+
const chat = await this._engine.getChat({ id: chatId }).catch(() => null);
|
|
2134
|
+
if (!chat)
|
|
2135
|
+
return { compacted: false, reason: 'unknown chat' };
|
|
2136
|
+
const agent = await this._engine.getAgent({ id: chat.agentId }).catch(() => null);
|
|
2137
|
+
if (!agent || !agent.providerDeviceId?.equals(deviceId)) {
|
|
2138
|
+
return { compacted: false, reason: 'not the provider device' };
|
|
2139
|
+
}
|
|
2140
|
+
const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
|
|
2141
|
+
const config = await this._engine.getConfig({}).catch(() => null);
|
|
2142
|
+
const ctxSize = agent.ctxSize || config?.ctxSize || undefined;
|
|
2143
|
+
if (ctxSize !== undefined && !canCompact(ctxSize)) {
|
|
2144
|
+
return { compacted: false, reason: 'below-floor' };
|
|
2145
|
+
}
|
|
2146
|
+
const stored = await listHistoryChunks(this._engine, chatId);
|
|
2147
|
+
const branches = historyBranches(stored);
|
|
2148
|
+
const chunks = stored.filter((chunk) => isPromptChunk(chunk, branches));
|
|
2149
|
+
const { chunks: priorAsks } = await this._engine
|
|
2150
|
+
.listChunks({ chatId, type: 'approval-request' })
|
|
2151
|
+
.catch(() => ({ chunks: [] }));
|
|
2152
|
+
const lastThroughId = lastCompactedThroughId(chunks);
|
|
2153
|
+
const lastThroughSeq = chunks.find((chunk) => chunk.id === lastThroughId)?.seq ?? -1;
|
|
2154
|
+
const userTurns = chunks.filter((chunk) => chunk.userMessage).length;
|
|
2155
|
+
let announced = false;
|
|
2156
|
+
let keepTurns = COMPACTION_KEEP_TURNS;
|
|
2157
|
+
for (;;) {
|
|
2158
|
+
const boundary = compactionBoundary(chunks, keepTurns);
|
|
2159
|
+
if (!boundary)
|
|
2160
|
+
return { compacted: false, reason: 'nothing-to-fold' };
|
|
2161
|
+
const through = chunks.find((chunk) => chunk.id === boundary.throughId);
|
|
2162
|
+
if (!through)
|
|
2163
|
+
return { compacted: false, reason: 'nothing-to-fold' };
|
|
2164
|
+
if (through.seq <= lastThroughSeq)
|
|
2165
|
+
return { compacted: false, reason: 'already-compacted' };
|
|
2166
|
+
const inRange = chunks.filter((chunk) => chunk.seq <= through.seq);
|
|
2167
|
+
const folded = historyFromChunks(inRange, staleToolRunIds(inRange, priorAsks ?? [], ''));
|
|
2168
|
+
if (!announced) {
|
|
2169
|
+
await options.onFold?.();
|
|
2170
|
+
announced = true;
|
|
2171
|
+
}
|
|
2172
|
+
const attempt = await this._summarize(folded, model, withConfigDefaults(agent, config), options.signal);
|
|
2173
|
+
if (!attempt.summary) {
|
|
2174
|
+
if (attempt.overflowed && boundary.foldedTurns > 1) {
|
|
2175
|
+
keepTurns = userTurns - Math.floor(boundary.foldedTurns / 2);
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
return { compacted: false, reason: 'the model produced no summary' };
|
|
2179
|
+
}
|
|
2180
|
+
const summary = attempt.summary;
|
|
2181
|
+
if (!foldReducesPrompt(folded, summary)) {
|
|
2182
|
+
return { compacted: false, reason: 'summary-not-smaller' };
|
|
2183
|
+
}
|
|
2184
|
+
if (options.signal?.aborted)
|
|
2185
|
+
return { compacted: false, reason: 'canceled' };
|
|
2186
|
+
await this._engine.appendChunk({
|
|
2187
|
+
chatId,
|
|
2188
|
+
type: 'compaction',
|
|
2189
|
+
compaction: { summary, throughId: boundary.throughId },
|
|
2190
|
+
originDeviceId: deviceId
|
|
2191
|
+
});
|
|
2192
|
+
await this._deleteChatKvCache(chatId, 'compaction');
|
|
2193
|
+
this._log?.info('runs', 'chat compacted', {
|
|
2194
|
+
chatId,
|
|
2195
|
+
foldedTurns: boundary.foldedTurns,
|
|
2196
|
+
summaryChars: summary.length
|
|
2197
|
+
});
|
|
2198
|
+
return { compacted: true, foldedTurns: boundary.foldedTurns };
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
async _summarize(turns, model, agent, signal) {
|
|
2202
|
+
const hasPriorSummary = turns[0]?.content.startsWith(COMPACTION_TURN_PREFIX) === true;
|
|
2203
|
+
const events = this._harness.completion({
|
|
2204
|
+
model,
|
|
2205
|
+
agent: {
|
|
2206
|
+
...agent,
|
|
2207
|
+
toolsScope: 'selected',
|
|
2208
|
+
enabledTools: [],
|
|
2209
|
+
skillsScope: 'selected',
|
|
2210
|
+
enabledSkills: []
|
|
2211
|
+
},
|
|
2212
|
+
messages: [
|
|
2213
|
+
{
|
|
2214
|
+
role: 'user',
|
|
2215
|
+
content: buildCompactionPrompt(turns.map((turn) => ({ role: turn.role, content: turn.content })), hasPriorSummary)
|
|
2216
|
+
}
|
|
2217
|
+
],
|
|
2218
|
+
generationParams: { reasoning_budget: 0 },
|
|
2219
|
+
signal: signal ?? this._lifecycle.signal
|
|
2220
|
+
});
|
|
2221
|
+
let text = '';
|
|
2222
|
+
let failed = null;
|
|
2223
|
+
for await (const event of events) {
|
|
2224
|
+
if (event.type === 'content' && typeof event.text === 'string')
|
|
2225
|
+
text += event.text;
|
|
2226
|
+
else if (event.type === 'error') {
|
|
2227
|
+
failed = typeof event.message === 'string' ? event.message : 'summarizer failed';
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
else if (event.type === 'aborted') {
|
|
2231
|
+
failed = 'summarizer aborted';
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
else if (event.type === 'done')
|
|
2235
|
+
break;
|
|
2236
|
+
}
|
|
2237
|
+
if (failed !== null) {
|
|
2238
|
+
this._log?.warn('runs', 'summarizer failed mid-stream', { chars: text.length, failed });
|
|
2239
|
+
return { summary: null, overflowed: isContextOverflowFailure(failed) };
|
|
2240
|
+
}
|
|
2241
|
+
return { summary: parseCompactionSummary(text), overflowed: false };
|
|
2242
|
+
}
|
|
2243
|
+
async _autoTitle(chatId, messages, model, agent, probe) {
|
|
2244
|
+
try {
|
|
2245
|
+
const first = messages.find((turn) => turn.role === 'user' && turn.content.trim());
|
|
2246
|
+
if (!first)
|
|
2247
|
+
return;
|
|
2248
|
+
const chat = await this._engine.getChat({ id: chatId });
|
|
2249
|
+
if (!isDefaultChatTitle(chat?.title))
|
|
2250
|
+
return;
|
|
2251
|
+
await this._generateChatTitle({ chatId, input: first.content, model, agent, probe });
|
|
2252
|
+
}
|
|
2253
|
+
catch (err) {
|
|
2254
|
+
this._log?.warn('runs', 'auto-title skipped', { chatId, err });
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
async _generateChatTitle(params) {
|
|
2258
|
+
const { chatId, input, model, agent, probe } = params;
|
|
2259
|
+
const prompt = input.trim();
|
|
2260
|
+
if (!prompt)
|
|
2261
|
+
return null;
|
|
2262
|
+
let generated = null;
|
|
2263
|
+
if (probe && !this.closing) {
|
|
2264
|
+
try {
|
|
2265
|
+
generated = await this._titleFromCompletion({
|
|
2266
|
+
input: prompt,
|
|
2267
|
+
model,
|
|
2268
|
+
agent,
|
|
2269
|
+
signal: this._lifecycle.signal
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
catch (err) {
|
|
2273
|
+
this._log?.warn('runs', 'title completion failed', { chatId, err });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
const title = generated ?? deriveChatTitle(prompt);
|
|
2277
|
+
if (!title)
|
|
2278
|
+
return null;
|
|
2279
|
+
await this._engine.renameChat({ id: chatId, title });
|
|
2280
|
+
this._log?.info('runs', 'chat titled', {
|
|
2281
|
+
chatId,
|
|
2282
|
+
title,
|
|
2283
|
+
source: generated ? 'model' : 'heuristic'
|
|
2284
|
+
});
|
|
2285
|
+
return title;
|
|
2286
|
+
}
|
|
2287
|
+
async _titleFromCompletion(params) {
|
|
2288
|
+
const { input, model, agent, signal } = params;
|
|
2289
|
+
const events = this._harness.completion({
|
|
2290
|
+
model,
|
|
2291
|
+
agent: {
|
|
2292
|
+
...agent,
|
|
2293
|
+
toolsScope: 'selected',
|
|
2294
|
+
enabledTools: [],
|
|
2295
|
+
skillsScope: 'selected',
|
|
2296
|
+
enabledSkills: []
|
|
2297
|
+
},
|
|
2298
|
+
messages: [{ role: 'user', content: buildTitlePrompt(input) }],
|
|
2299
|
+
generationParams: { reasoning_budget: 0 },
|
|
2300
|
+
responseFormat: {
|
|
2301
|
+
type: 'json_schema',
|
|
2302
|
+
json_schema: {
|
|
2303
|
+
name: 'chat_title',
|
|
2304
|
+
schema: {
|
|
2305
|
+
type: 'object',
|
|
2306
|
+
properties: { title: { type: 'string' } },
|
|
2307
|
+
required: ['title'],
|
|
2308
|
+
additionalProperties: false
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
signal
|
|
2313
|
+
});
|
|
2314
|
+
let text = '';
|
|
2315
|
+
for await (const event of events) {
|
|
2316
|
+
if (event.type === 'content' && typeof event.text === 'string')
|
|
2317
|
+
text += event.text;
|
|
2318
|
+
else if (event.type === 'error' || event.type === 'aborted' || event.type === 'done')
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
return parseTitleResponse(text);
|
|
2322
|
+
}
|
|
2323
|
+
_createApprove(chatId, runId, deviceId, agentId, hasRun) {
|
|
2324
|
+
const waiters = new Map();
|
|
2325
|
+
let latestChunks = [];
|
|
2326
|
+
let watcher = null;
|
|
2327
|
+
let closing = false;
|
|
2328
|
+
const pump = async (source) => {
|
|
2329
|
+
while (true) {
|
|
2330
|
+
const { value, done } = await source.next();
|
|
2331
|
+
if (done)
|
|
2332
|
+
return;
|
|
2333
|
+
latestChunks = value.chunks ?? [];
|
|
2334
|
+
for (const chunk of latestChunks) {
|
|
2335
|
+
const status = chunk.approvalRequest?.status;
|
|
2336
|
+
if (!status || status === 'pending')
|
|
2337
|
+
continue;
|
|
2338
|
+
const waiter = waiters.get(chunk.id);
|
|
2339
|
+
if (!waiter)
|
|
2340
|
+
continue;
|
|
2341
|
+
waiters.delete(chunk.id);
|
|
2342
|
+
this._log?.info('approvals', `answer ${status}`, {
|
|
2343
|
+
runId,
|
|
2344
|
+
chatId,
|
|
2345
|
+
chunkId: chunk.id,
|
|
2346
|
+
tool: waiter.tool
|
|
2347
|
+
});
|
|
2348
|
+
waiter.resolve(status === 'approved');
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
const ensureWatch = () => {
|
|
2353
|
+
if (watcher)
|
|
2354
|
+
return;
|
|
2355
|
+
const opened = this._engine
|
|
2356
|
+
.chunksWatch({ chatId, type: 'approval-request' })[Symbol.asyncIterator]();
|
|
2357
|
+
watcher = opened;
|
|
2358
|
+
pump(opened)
|
|
2359
|
+
.catch((err) => {
|
|
2360
|
+
if (closing)
|
|
2361
|
+
this._log?.debug('assistant', 'approval watch closed', err);
|
|
2362
|
+
else
|
|
2363
|
+
this._log?.error('assistant', 'approval watch failed', err);
|
|
2364
|
+
})
|
|
2365
|
+
.finally(() => {
|
|
2366
|
+
for (const [id, waiter] of waiters) {
|
|
2367
|
+
this._log?.warn('approvals', 'deny: answer watch closed', {
|
|
2368
|
+
runId,
|
|
2369
|
+
chunkId: id,
|
|
2370
|
+
tool: waiter.tool
|
|
2371
|
+
});
|
|
2372
|
+
waiter.resolve(false);
|
|
2373
|
+
}
|
|
2374
|
+
if (watcher === opened)
|
|
2375
|
+
watcher = null;
|
|
2376
|
+
});
|
|
2377
|
+
};
|
|
2378
|
+
const asksForRun = async () => {
|
|
2379
|
+
const { chunks } = await this._engine
|
|
2380
|
+
.listChunks({ chatId, type: 'approval-request' })
|
|
2381
|
+
.catch(() => ({ chunks: [] }));
|
|
2382
|
+
return (chunks ?? []).filter((chunk) => chunk.runId === runId);
|
|
2383
|
+
};
|
|
2384
|
+
const sameCall = (chunk, call) => {
|
|
2385
|
+
const ask = chunk.approvalRequest;
|
|
2386
|
+
if (!ask || ask.name !== call.name)
|
|
2387
|
+
return false;
|
|
2388
|
+
return ((ask.resource?.kind ?? null) === (call.resource?.kind ?? null) &&
|
|
2389
|
+
(ask.resource?.label ?? null) === (call.resource?.label ?? null) &&
|
|
2390
|
+
JSON.stringify(ask.arguments) === JSON.stringify(call.arguments));
|
|
2391
|
+
};
|
|
2392
|
+
const park = (chunkId, tool) => {
|
|
2393
|
+
const status = latestChunks.find((c) => c.id === chunkId)?.approvalRequest?.status;
|
|
2394
|
+
if (status && status !== 'pending') {
|
|
2395
|
+
this._log?.info('approvals', `answer ${status} (raced the ask)`, { runId, chunkId, tool });
|
|
2396
|
+
return Promise.resolve(status === 'approved');
|
|
2397
|
+
}
|
|
2398
|
+
return new Promise((resolve) => waiters.set(chunkId, { resolve, tool }));
|
|
2399
|
+
};
|
|
2400
|
+
const approve = async (call) => {
|
|
2401
|
+
const { rules } = await this._engine.listApprovalRules({}).catch((err) => {
|
|
2402
|
+
this._log?.warn('approvals', 'rule lookup failed, asking instead', { runId, err });
|
|
2403
|
+
return { rules: [] };
|
|
2404
|
+
});
|
|
2405
|
+
const rule = matchApprovalRule(rules ?? [], {
|
|
2406
|
+
toolName: call.name,
|
|
2407
|
+
resource: call.resource,
|
|
2408
|
+
agentId
|
|
2409
|
+
});
|
|
2410
|
+
if (rule) {
|
|
2411
|
+
this._log?.info('approvals', `rule ${rule.decision}`, {
|
|
2412
|
+
runId,
|
|
2413
|
+
chatId,
|
|
2414
|
+
tool: call.name,
|
|
2415
|
+
resource: call.resource?.label
|
|
2416
|
+
});
|
|
2417
|
+
return rule.decision === 'allow';
|
|
2418
|
+
}
|
|
2419
|
+
ensureWatch();
|
|
2420
|
+
const prior = (await asksForRun()).findLast((chunk) => sameCall(chunk, call));
|
|
2421
|
+
if (prior) {
|
|
2422
|
+
const status = prior.approvalRequest?.status;
|
|
2423
|
+
if (!status || status === 'pending') {
|
|
2424
|
+
this._log?.info('approvals', 'rebinding the ask this run parked on', {
|
|
2425
|
+
runId,
|
|
2426
|
+
chatId,
|
|
2427
|
+
chunkId: prior.id,
|
|
2428
|
+
tool: call.name
|
|
2429
|
+
});
|
|
2430
|
+
return park(prior.id, call.name);
|
|
2431
|
+
}
|
|
2432
|
+
if (!hasRun()) {
|
|
2433
|
+
this._log?.info('approvals', `answer ${status} (from before the app closed)`, {
|
|
2434
|
+
runId,
|
|
2435
|
+
chatId,
|
|
2436
|
+
chunkId: prior.id,
|
|
2437
|
+
tool: call.name
|
|
2438
|
+
});
|
|
2439
|
+
return status === 'approved';
|
|
2440
|
+
}
|
|
2441
|
+
this._log?.info('approvals', 'asking again: the replay would repeat a call that ran', {
|
|
2442
|
+
runId,
|
|
2443
|
+
chatId,
|
|
2444
|
+
priorChunkId: prior.id,
|
|
2445
|
+
tool: call.name
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
const approvalRequest = {
|
|
2449
|
+
status: 'pending',
|
|
2450
|
+
name: call.name,
|
|
2451
|
+
arguments: call.arguments,
|
|
2452
|
+
resource: call.resource ?? undefined,
|
|
2453
|
+
agentId
|
|
2454
|
+
};
|
|
2455
|
+
const chunk = await this._engine.appendChunk({
|
|
2456
|
+
chatId,
|
|
2457
|
+
runId,
|
|
2458
|
+
type: 'approval-request',
|
|
2459
|
+
approvalRequest,
|
|
2460
|
+
originDeviceId: deviceId,
|
|
2461
|
+
effect: call.effect === 'read' ? 'read' : 'write',
|
|
2462
|
+
intent: call.intent ?? undefined,
|
|
2463
|
+
display: call.display ?? undefined
|
|
2464
|
+
});
|
|
2465
|
+
this._log?.info('approvals', 'ask', {
|
|
2466
|
+
runId,
|
|
2467
|
+
chatId,
|
|
2468
|
+
chunkId: chunk.id,
|
|
2469
|
+
tool: call.name,
|
|
2470
|
+
resource: call.resource?.label
|
|
2471
|
+
});
|
|
2472
|
+
return park(chunk.id, call.name);
|
|
2473
|
+
};
|
|
2474
|
+
const settle = async ({ preservePending = false } = {}) => {
|
|
2475
|
+
if (!preservePending) {
|
|
2476
|
+
for (const ask of await asksForRun()) {
|
|
2477
|
+
if (ask.approvalRequest?.status !== 'pending')
|
|
2478
|
+
continue;
|
|
2479
|
+
this._log?.warn('approvals', 'deny: unanswered at run end', {
|
|
2480
|
+
runId,
|
|
2481
|
+
chunkId: ask.id,
|
|
2482
|
+
tool: ask.approvalRequest?.name
|
|
2483
|
+
});
|
|
2484
|
+
await this._engine.resolveApproval({ id: ask.id, approved: false });
|
|
2485
|
+
}
|
|
2486
|
+
for (const waiter of waiters.values())
|
|
2487
|
+
waiter.resolve(false);
|
|
2488
|
+
}
|
|
2489
|
+
waiters.clear();
|
|
2490
|
+
closing = true;
|
|
2491
|
+
await watcher?.return?.();
|
|
2492
|
+
watcher = null;
|
|
2493
|
+
};
|
|
2494
|
+
return { approve, settle };
|
|
2495
|
+
}
|
|
2496
|
+
_abortOutcome(runId, reason) {
|
|
2497
|
+
const known = knownAbortOutcome(reason);
|
|
2498
|
+
if (reason === RUNNER_DIED && this._harnessDeath) {
|
|
2499
|
+
return { kind: 'failed', reason: this._harnessDeath };
|
|
2500
|
+
}
|
|
2501
|
+
if (known)
|
|
2502
|
+
return known;
|
|
2503
|
+
if (this.suspended)
|
|
2504
|
+
return { kind: 'replay' };
|
|
2505
|
+
if (reason !== undefined) {
|
|
2506
|
+
this._log?.warn('runs', 'run aborted for an unrecognised reason', { runId, reason });
|
|
2507
|
+
}
|
|
2508
|
+
return { kind: 'failed', reason: RUN_FAILURE_REASONS.runtimeEndedTurn };
|
|
2509
|
+
}
|
|
2510
|
+
_watchSettled(chatId, runId, controller) {
|
|
2511
|
+
const it = this._engine.chunksWatch({ chatId, type: 'run-status' })[Symbol.asyncIterator]();
|
|
2512
|
+
const watching = (async () => {
|
|
2513
|
+
while (true) {
|
|
2514
|
+
const { value, done } = await it.next();
|
|
2515
|
+
if (done)
|
|
2516
|
+
return;
|
|
2517
|
+
const settled = (value.chunks ?? []).some((c) => c.runId === runId && TERMINAL_STATES.has(c.runStatus?.state ?? ''));
|
|
2518
|
+
if (settled) {
|
|
2519
|
+
controller.abort(RUN_SETTLED);
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
})();
|
|
2524
|
+
watching.catch((err) => {
|
|
2525
|
+
if (this.closing || isStreamDestroyed(err))
|
|
2526
|
+
return;
|
|
2527
|
+
this._log?.error('assistant', 'settled watch failed', err);
|
|
2528
|
+
});
|
|
2529
|
+
return {
|
|
2530
|
+
stop: () => {
|
|
2531
|
+
it.return?.()?.catch?.(noop);
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
_watchOwnership(agentId, controller) {
|
|
2536
|
+
const deviceId = this._deviceId;
|
|
2537
|
+
const it = this._engine.agentsWatch({})[Symbol.asyncIterator]();
|
|
2538
|
+
const watching = (async () => {
|
|
2539
|
+
while (true) {
|
|
2540
|
+
const { value, done } = await it.next();
|
|
2541
|
+
if (done)
|
|
2542
|
+
return;
|
|
2543
|
+
const row = (value.agents ?? []).find((a) => a.id === agentId);
|
|
2544
|
+
if (!row || row.deletedAt || !row.providerDeviceId?.equals(deviceId)) {
|
|
2545
|
+
controller.abort(OWNERSHIP_LOST);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
})();
|
|
2550
|
+
watching.catch((err) => {
|
|
2551
|
+
if (this.closing || isStreamDestroyed(err))
|
|
2552
|
+
return;
|
|
2553
|
+
this._log?.error('assistant', 'ownership watch failed', err);
|
|
2554
|
+
});
|
|
2555
|
+
return {
|
|
2556
|
+
stop: () => {
|
|
2557
|
+
it.return?.()?.catch?.(noop);
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
_yieldDownloads() {
|
|
2562
|
+
this._downloads?.yieldToRuns([...this._runs.values()].filter((run) => run.executing).map((run) => run.model));
|
|
2563
|
+
}
|
|
2564
|
+
_logModelStatus(state) {
|
|
2565
|
+
if (typeof state !== 'object' || state === null)
|
|
2566
|
+
return;
|
|
2567
|
+
if (!('status' in state) || typeof state.status !== 'string')
|
|
2568
|
+
return;
|
|
2569
|
+
if (!('model' in state) || typeof state.model !== 'string')
|
|
2570
|
+
return;
|
|
2571
|
+
const { status, model } = state;
|
|
2572
|
+
if (status === 'downloading' || this._modelStatusLogged.get(model) === status)
|
|
2573
|
+
return;
|
|
2574
|
+
this._modelStatusLogged.set(model, status);
|
|
2575
|
+
this._log?.info('models', `model ${status}`, { model });
|
|
2576
|
+
}
|
|
2577
|
+
_modelHolder(runId, chatId, model) {
|
|
2578
|
+
for (const [id, run] of this._runs) {
|
|
2579
|
+
if (id !== runId && run.executing && run.chatId !== chatId && run.model === model) {
|
|
2580
|
+
return run.chatId;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return null;
|
|
2584
|
+
}
|
|
2585
|
+
_watchModelProgress(update, queued = false) {
|
|
2586
|
+
let last = '';
|
|
2587
|
+
let showing = queued;
|
|
2588
|
+
let transcribing = false;
|
|
2589
|
+
let documents = false;
|
|
2590
|
+
const apply = (progress) => {
|
|
2591
|
+
const runStatus = progress
|
|
2592
|
+
? { state: 'executing', progress }
|
|
2593
|
+
: { state: 'executing' };
|
|
2594
|
+
if (documents)
|
|
2595
|
+
runStatus.reason = 'preparing-documents';
|
|
2596
|
+
const key = JSON.stringify(runStatus);
|
|
2597
|
+
if (key === last)
|
|
2598
|
+
return;
|
|
2599
|
+
last = key;
|
|
2600
|
+
showing = progress !== null || documents;
|
|
2601
|
+
update(runStatus).catch(noop);
|
|
2602
|
+
};
|
|
2603
|
+
const onModel = (state) => {
|
|
2604
|
+
if (state.status === 'loading')
|
|
2605
|
+
apply({ phase: 'loading', model: state.model });
|
|
2606
|
+
else if (state.status === 'downloading') {
|
|
2607
|
+
const percentage = Math.floor((state.percentage ?? 0) / 10) * 10;
|
|
2608
|
+
apply({ phase: 'downloading', model: state.model, percentage });
|
|
2609
|
+
}
|
|
2610
|
+
else
|
|
2611
|
+
apply(transcribing ? { phase: 'transcribing' } : null);
|
|
2612
|
+
};
|
|
2613
|
+
if (this._harness.on && this._harness.off)
|
|
2614
|
+
this._harness.on('model', onModel);
|
|
2615
|
+
return {
|
|
2616
|
+
documents: (active) => {
|
|
2617
|
+
documents = active;
|
|
2618
|
+
apply(null);
|
|
2619
|
+
},
|
|
2620
|
+
transcribe: (active) => {
|
|
2621
|
+
transcribing = active;
|
|
2622
|
+
apply(active ? { phase: 'transcribing' } : null);
|
|
2623
|
+
},
|
|
2624
|
+
image: (progress) => {
|
|
2625
|
+
if (!progress)
|
|
2626
|
+
return apply(null);
|
|
2627
|
+
apply({ phase: 'generating', percentage: Math.floor(progress.percentage / 10) * 10 });
|
|
2628
|
+
},
|
|
2629
|
+
stop: () => this._harness.off?.('model', onModel),
|
|
2630
|
+
clear: () => {
|
|
2631
|
+
if (!showing)
|
|
2632
|
+
return;
|
|
2633
|
+
showing = false;
|
|
2634
|
+
last = JSON.stringify({ state: 'executing' });
|
|
2635
|
+
update({ state: 'executing' }).catch(noop);
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function noop() { }
|
|
2641
|
+
function runMetricsSummary(metrics) {
|
|
2642
|
+
const { timeToFirstToken, promptTokens, promptTokensPerSecond, cacheTokens, tokensPerSecond, totalTokens, durationMs } = metrics;
|
|
2643
|
+
return {
|
|
2644
|
+
timeToFirstToken,
|
|
2645
|
+
promptTokens,
|
|
2646
|
+
promptTokensPerSecond,
|
|
2647
|
+
cacheTokens,
|
|
2648
|
+
tokensPerSecond,
|
|
2649
|
+
totalTokens,
|
|
2650
|
+
durationMs
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
function isStreamDestroyed(err) {
|
|
2654
|
+
return typeof err === 'object' && err !== null && 'code' in err && err.code === 'STREAM_DESTROYED';
|
|
2655
|
+
}
|
|
2656
|
+
function harnessDeathReason(status) {
|
|
2657
|
+
const HARNESS_DIED = RUN_FAILURE_REASONS.harnessDied;
|
|
2658
|
+
if (typeof status !== 'object' || status === null)
|
|
2659
|
+
return HARNESS_DIED;
|
|
2660
|
+
const record = status;
|
|
2661
|
+
const detail = typeof record.error === 'string' && record.error ? record.error : null;
|
|
2662
|
+
const code = typeof record.code === 'string' && record.code ? record.code : null;
|
|
2663
|
+
if (!detail)
|
|
2664
|
+
return code ? `${HARNESS_DIED} (${code})` : HARNESS_DIED;
|
|
2665
|
+
return code ? `${HARNESS_DIED}: ${detail} (${code})` : `${HARNESS_DIED}: ${detail}`;
|
|
2666
|
+
}
|
|
2667
|
+
function harnessDied(status) {
|
|
2668
|
+
if (typeof status !== 'object' || status === null || !('state' in status))
|
|
2669
|
+
return false;
|
|
2670
|
+
return status.state === 'died';
|
|
2671
|
+
}
|
|
2672
|
+
function modelCatalogChanged(state) {
|
|
2673
|
+
return (typeof state === 'object' &&
|
|
2674
|
+
state !== null &&
|
|
2675
|
+
'catalogChanged' in state &&
|
|
2676
|
+
state.catalogChanged === true);
|
|
2677
|
+
}
|
|
2678
|
+
function imageDownloadProgress(state) {
|
|
2679
|
+
if (typeof state !== 'object' ||
|
|
2680
|
+
state === null ||
|
|
2681
|
+
!('status' in state) ||
|
|
2682
|
+
(state.status !== 'downloading' && state.status !== 'downloaded') ||
|
|
2683
|
+
!('model' in state) ||
|
|
2684
|
+
typeof state.model !== 'string' ||
|
|
2685
|
+
!('percentage' in state) ||
|
|
2686
|
+
typeof state.percentage !== 'number' ||
|
|
2687
|
+
!Number.isFinite(state.percentage) ||
|
|
2688
|
+
!('downloaded' in state) ||
|
|
2689
|
+
typeof state.downloaded !== 'number' ||
|
|
2690
|
+
!Number.isFinite(state.downloaded) ||
|
|
2691
|
+
!('total' in state) ||
|
|
2692
|
+
typeof state.total !== 'number' ||
|
|
2693
|
+
!Number.isFinite(state.total)) {
|
|
2694
|
+
return null;
|
|
2695
|
+
}
|
|
2696
|
+
const model = diffusionDownloadName(state.model);
|
|
2697
|
+
if (!model)
|
|
2698
|
+
return null;
|
|
2699
|
+
return {
|
|
2700
|
+
model,
|
|
2701
|
+
phase: state.status === 'downloading' ? 'downloading' : 'complete',
|
|
2702
|
+
percentage: state.percentage,
|
|
2703
|
+
downloaded: state.downloaded,
|
|
2704
|
+
total: state.total
|
|
2705
|
+
};
|
|
2706
|
+
}
|