@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +12 -8
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/CLI_SHELL.md +163 -53
  6. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  7. package/docs/RELEASES.md +16 -7
  8. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  9. package/docs/TEST_TIERS.md +26 -0
  10. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  11. package/package.json +45 -12
  12. package/packages/agentsam-repository/README.md +15 -0
  13. package/packages/agentsam-repository/package.json +25 -0
  14. package/packages/agentsam-repository/src/contracts.js +113 -0
  15. package/packages/agentsam-repository/src/index.js +3 -0
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  17. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  18. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  19. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  20. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  21. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  22. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  23. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  24. package/packages/connectors/cloudflare/package.json +10 -0
  25. package/packages/connectors/cloudflare/src/index.js +127 -0
  26. package/packages/connectors/cloudflare/src/owner.js +76 -0
  27. package/packages/connectors/cloudflare/src/routes.js +223 -0
  28. package/packages/connectors/cloudflare/src/vault.js +80 -0
  29. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  30. package/packages/identity/package.json +2 -2
  31. package/packages/identity/src/contracts/auth-config.js +18 -7
  32. package/packages/identity/tests/auth-config.test.mjs +9 -5
  33. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  34. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  35. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  36. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  37. package/protocol/README.md +1 -0
  38. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  39. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  40. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  41. package/protocol/capabilities/manifest.json +47 -0
  42. package/protocol/context/context-budget.schema.json +10 -15
  43. package/protocol/context/context-item.schema.json +4 -5
  44. package/protocol/context/resolved-context-pack.schema.json +19 -14
  45. package/protocol/models/README.md +373 -0
  46. package/protocol/models/model-inventory-v2.schema.json +212 -0
  47. package/protocol/repository/repository-contract.schema.json +24 -0
  48. package/protocol/repository/repository-dependency.schema.json +24 -0
  49. package/protocol/repository/repository-identity.schema.json +17 -0
  50. package/protocol/rpc/v1/common.proto +16 -0
  51. package/protocol/rpc/v1/errors.proto +35 -0
  52. package/protocol/rpc/v1/knowledge.proto +77 -0
  53. package/services/knowledge/package-lock.json +333 -0
  54. package/services/knowledge/package.json +5 -1
  55. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  56. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  57. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  58. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  59. package/skills/catalog.json +18 -0
  60. package/src/agent/capability-adapter.js +25 -13
  61. package/src/agent/index.js +1 -0
  62. package/src/agent/responses-runner.js +353 -0
  63. package/src/capabilities/repository-snapshot.js +3 -3
  64. package/src/cli.js +118 -31
  65. package/src/cloudflare/cpu-profile.js +115 -0
  66. package/src/cloudflare/index.js +14 -0
  67. package/src/cloudflare/wrangler.js +132 -0
  68. package/src/commands/account-auth.js +47 -0
  69. package/src/commands/cloudflare.js +58 -0
  70. package/src/commands/connections.js +93 -0
  71. package/src/commands/context-economics.js +129 -0
  72. package/src/commands/context.js +1 -1
  73. package/src/commands/db.js +20 -3
  74. package/src/commands/deploy.js +39 -3
  75. package/src/commands/env.js +90 -0
  76. package/src/commands/eval.js +63 -0
  77. package/src/commands/interactive.js +2 -5
  78. package/src/commands/knowledge.js +12 -4
  79. package/src/commands/merkle-persist.js +30 -11
  80. package/src/commands/merkle.js +1 -1
  81. package/src/commands/models.js +149 -46
  82. package/src/commands/ollama.js +26 -0
  83. package/src/commands/preferences.js +130 -61
  84. package/src/commands/resume.js +67 -0
  85. package/src/commands/security.js +5 -3
  86. package/src/commands/shell.js +568 -119
  87. package/src/commands/tunnel.js +2 -2
  88. package/src/commands/whoami.js +86 -0
  89. package/src/context/budget.js +68 -6
  90. package/src/context/index.js +3 -1
  91. package/src/context/rehydrate.js +35 -0
  92. package/src/context/resolve.js +44 -12
  93. package/src/errors/contract.js +236 -0
  94. package/src/errors/diagnostic.js +160 -0
  95. package/src/errors/index.js +23 -0
  96. package/src/eval/context.js +191 -0
  97. package/src/eval/index.js +1 -0
  98. package/src/index.js +68 -2
  99. package/src/knowledge/service/auth.js +13 -0
  100. package/src/knowledge/service/grpc-client.js +115 -0
  101. package/src/knowledge/service/grpc-codec.js +237 -0
  102. package/src/knowledge/service/grpc-server.js +83 -0
  103. package/src/knowledge/service/job-engine.js +248 -0
  104. package/src/knowledge/service/server.js +87 -135
  105. package/src/knowledge/source.js +1 -1
  106. package/src/lib/account-session.js +98 -0
  107. package/src/lib/agent-instructions.js +73 -0
  108. package/src/lib/auth.js +4 -0
  109. package/src/lib/cli-preferences.js +55 -24
  110. package/src/lib/deploy/git-guard.js +69 -0
  111. package/src/lib/deploy/health.js +57 -0
  112. package/src/lib/deploy/local-studio.js +283 -0
  113. package/src/lib/deploy/secret-scan.js +65 -0
  114. package/src/lib/deploy-receipt/index.js +2 -2
  115. package/src/lib/detect-context.js +2 -2
  116. package/src/lib/execution-approvals.js +59 -0
  117. package/src/lib/knowledge-docker.js +6 -3
  118. package/src/lib/local-sessions.js +148 -0
  119. package/src/lib/local-status.js +1 -1
  120. package/src/lib/project-config.js +1 -1
  121. package/src/lib/provider-credentials.js +183 -0
  122. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  123. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  124. package/src/lib/slash-commands.js +23 -16
  125. package/src/local/migrations.js +93 -0
  126. package/src/local/runtime-store.js +141 -0
  127. package/src/local/sqlite.js +2 -0
  128. package/src/local-pty/server.js +113 -51
  129. package/src/models/catalog.js +135 -0
  130. package/src/models/discovery.js +292 -0
  131. package/src/models/index.js +7 -0
  132. package/src/providers/anthropic-messages.js +192 -0
  133. package/src/providers/cloudflare-chat.js +183 -0
  134. package/src/providers/factory.js +69 -0
  135. package/src/providers/gemini-generate-content.js +208 -0
  136. package/src/providers/index.js +10 -0
  137. package/src/providers/ollama-chat.js +148 -0
  138. package/src/providers/openai-responses.js +426 -0
  139. package/src/repository/index.js +14 -2
  140. package/src/rpc/generated/common_grpc_pb.js +1 -0
  141. package/src/rpc/generated/common_pb.js +536 -0
  142. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  143. package/src/rpc/generated/errors_pb.js +482 -0
  144. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  145. package/src/rpc/generated/knowledge_pb.js +2168 -0
  146. package/src/rpc/generated/package.json +3 -0
  147. package/src/security/process.js +35 -9
  148. package/src/security/trust-boundary.js +2 -2
  149. package/src/telemetry/contracts.js +203 -0
  150. package/src/telemetry/events.js +51 -0
  151. package/src/telemetry/index.js +8 -0
  152. package/src/tools/hydrate.js +35 -0
  153. package/src/tools/index.js +1 -0
  154. package/src/ui/boot.js +15 -17
  155. package/src/ui/cli/activity.js +76 -0
  156. package/src/ui/cli/compaction.js +15 -0
  157. package/src/ui/cli/footer.js +39 -0
  158. package/src/ui/cli/help.js +192 -0
  159. package/src/ui/cli/plan.js +20 -0
  160. package/src/ui/cli/runtime-events.js +110 -0
  161. package/src/ui/cli/waiting.js +16 -0
  162. package/src/ui/merkle/render.js +1 -1
  163. package/test/account-session.test.mjs +36 -0
  164. package/test/cli/preferences-runtime.test.mjs +11 -0
  165. package/test/cli/runtime-ui.test.mjs +74 -0
  166. package/test/cli-preferences.test.mjs +26 -5
  167. package/test/cloudflare-connector.test.mjs +96 -0
  168. package/test/cloudflare-runtime.test.mjs +75 -0
  169. package/test/context.test.mjs +61 -12
  170. package/test/deploy-health-scan.test.mjs +67 -0
  171. package/test/error-diagnostics.test.mjs +115 -0
  172. package/test/eval-context.test.mjs +37 -0
  173. package/test/execution-approvals.test.mjs +27 -0
  174. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  175. package/test/integration/cli-help.test.mjs +37 -0
  176. package/test/integration/knowledge-rpc.test.mjs +112 -0
  177. package/test/integration/merkle-cli.test.mjs +61 -0
  178. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  179. package/test/integration/provider-env-cli.test.mjs +49 -0
  180. package/test/integration/provider-factory.test.mjs +197 -0
  181. package/test/integration/repository-company-graph.test.mjs +90 -0
  182. package/test/integration/runtime-migrations.test.mjs +82 -0
  183. package/test/knowledge-service.test.mjs +5 -0
  184. package/test/knowledge.test.mjs +16 -0
  185. package/test/live/terminal-transport.live.test.mjs +24 -0
  186. package/test/local-sessions.test.mjs +48 -0
  187. package/test/local-studio-deploy.test.mjs +83 -0
  188. package/test/model-catalog.test.mjs +43 -0
  189. package/test/models.test.mjs +127 -16
  190. package/test/npm10-lock.test.mjs +29 -0
  191. package/test/ollama.test.mjs +21 -0
  192. package/test/openai-responses.test.mjs +95 -0
  193. package/test/portable-context.test.mjs +1 -1
  194. package/test/provider-credentials.test.mjs +96 -0
  195. package/test/rehydrate.test.mjs +25 -0
  196. package/test/release-hygiene.test.mjs +13 -5
  197. package/test/responses-runner.test.mjs +150 -0
  198. package/test/shell.test.mjs +92 -23
  199. package/test/smoke.mjs +4 -1
  200. package/test/telemetry.test.mjs +79 -0
  201. package/test/terminal/local-pty.mock.test.mjs +151 -0
  202. package/test/tools-search.test.mjs +14 -1
  203. package/test/whoami-resume.test.mjs +56 -0
  204. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  205. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  206. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  207. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  208. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  209. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  210. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  211. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -0,0 +1,237 @@
1
+ import grpc from '@grpc/grpc-js';
2
+ import { createRequire } from 'node:module';
3
+ import {
4
+ ERROR_CODE,
5
+ ERROR_REASON,
6
+ canonicalCodeFromHttpStatus,
7
+ createErrorEnvelope,
8
+ defaultHttpStatusForCode,
9
+ grpcStatusForCode,
10
+ } from '../../errors/contract.js';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const knowledgePb = require('../../rpc/generated/knowledge_pb.js');
14
+ const errorsPb = require('../../rpc/generated/errors_pb.js');
15
+ const { KnowledgeServiceService, KnowledgeServiceClient } = require('../../rpc/generated/knowledge_grpc_pb.js');
16
+ const { Timestamp } = require('google-protobuf/google/protobuf/timestamp_pb.js');
17
+
18
+ const operationToProto = {
19
+ index: knowledgePb.JobOperation.JOB_OPERATION_INDEX,
20
+ plan: knowledgePb.JobOperation.JOB_OPERATION_PLAN,
21
+ search: knowledgePb.JobOperation.JOB_OPERATION_SEARCH,
22
+ };
23
+ const operationFromProto = new Map(Object.entries(operationToProto).map(([key, value]) => [value, key]));
24
+ const statusToProto = {
25
+ queued: knowledgePb.JobStatus.JOB_STATUS_QUEUED,
26
+ running: knowledgePb.JobStatus.JOB_STATUS_RUNNING,
27
+ completed: knowledgePb.JobStatus.JOB_STATUS_COMPLETED,
28
+ failed: knowledgePb.JobStatus.JOB_STATUS_FAILED,
29
+ };
30
+ const statusFromProto = new Map(Object.entries(statusToProto).map(([key, value]) => [value, key]));
31
+ const protoErrorCode = code => errorsPb.ErrorCode[`ERROR_CODE_${code}`] ?? errorsPb.ErrorCode.ERROR_CODE_UNKNOWN;
32
+ const errorCodeNames = new Map(Object.entries(errorsPb.ErrorCode).map(([key, value]) => [value, key.replace(/^ERROR_CODE_/, '')]));
33
+
34
+ const grpcToHttp = new Map(Object.entries({
35
+ 1: 499, 2: 500, 3: 400, 4: 504, 5: 404, 6: 409, 7: 403, 8: 429,
36
+ 9: 400, 10: 409, 11: 400, 12: 501, 13: 500, 14: 503, 15: 500, 16: 401,
37
+ }).map(([key, value]) => [Number(key), value]));
38
+
39
+ function timestamp(value) {
40
+ const message = new Timestamp();
41
+ message.fromDate(new Date(value));
42
+ return message;
43
+ }
44
+ function iso(message) { return message ? message.toDate().toISOString() : null; }
45
+ function jobId(value) {
46
+ const id = new knowledgePb.JobId();
47
+ id.setValue(value || '');
48
+ return id;
49
+ }
50
+
51
+ export { grpc, knowledgePb, errorsPb, KnowledgeServiceService, KnowledgeServiceClient };
52
+
53
+ export function encodeJob(job) {
54
+ const message = new knowledgePb.Job();
55
+ message.setId(jobId(job.id));
56
+ message.setStatus(statusToProto[job.status] ?? knowledgePb.JobStatus.JOB_STATUS_UNSPECIFIED);
57
+ message.setAttempts(job.attempts || 0);
58
+ message.setCreatedAt(timestamp(job.created_at));
59
+ message.setUpdatedAt(timestamp(job.updated_at));
60
+ if (job.result !== null && job.result !== undefined) message.setResultJson(JSON.stringify(job.result));
61
+ if (job.error) {
62
+ const failure = new errorsPb.ErrorDetail();
63
+ failure.setCode(protoErrorCode(ERROR_CODE.INTERNAL));
64
+ failure.setMessage(job.error);
65
+ failure.setRetryable(false);
66
+ failure.setReason(ERROR_REASON.EXECUTION_FAILED);
67
+ failure.setHttpStatus(500);
68
+ message.setFailure(failure);
69
+ }
70
+ return message;
71
+ }
72
+
73
+ export function decodeJob(message) {
74
+ const resultJson = message.getResultJson();
75
+ const failure = message.hasFailure() ? message.getFailure() : null;
76
+ return {
77
+ id: message.hasId() ? message.getId().getValue() : '',
78
+ status: statusFromProto.get(message.getStatus()) || 'unknown',
79
+ attempts: message.getAttempts(),
80
+ created_at: iso(message.getCreatedAt()),
81
+ updated_at: iso(message.getUpdatedAt()),
82
+ result: resultJson ? JSON.parse(resultJson) : null,
83
+ error: failure ? failure.getMessage() : null,
84
+ };
85
+ }
86
+
87
+ export function encodeRepositoryList(value) {
88
+ const response = new knowledgePb.ListRepositoriesResponse();
89
+ response.setRepositoriesList(value.repositories.map(repo => {
90
+ const ref = new knowledgePb.RepositoryRef();
91
+ ref.setAlias(repo.alias);
92
+ ref.setRepositoryId(repo.repository_id || '');
93
+ return ref;
94
+ }));
95
+ response.setEmbeddingsEnabled(value.embeddings_enabled);
96
+ return response;
97
+ }
98
+
99
+ export function decodeRepositoryList(message) {
100
+ const details = message.getRepositoriesList().map(repo => ({ alias: repo.getAlias(), repository_id: repo.getRepositoryId() }));
101
+ return { repositories: details.map(repo => repo.alias), repository_details: details, embeddings_enabled: message.getEmbeddingsEnabled() };
102
+ }
103
+
104
+ export function decodeSubmitRequest(message) {
105
+ const body = { repository: message.getRepository(), operation: operationFromProto.get(message.getOperation()) };
106
+ if (message.hasScope()) body.scope = message.getScope();
107
+ if (message.hasInclude()) body.include = message.getInclude().getValuesList();
108
+ if (message.hasExclude()) body.exclude = message.getExclude().getValuesList();
109
+ if (message.hasEmbed()) body.embed = message.getEmbed();
110
+ if (message.hasSemantic()) body.semantic = message.getSemantic();
111
+ if (message.hasQuery()) body.query = message.getQuery();
112
+ if (message.hasTopK()) body.top_k = message.getTopK();
113
+ if (message.hasTokenBudget()) body.token_budget = message.getTokenBudget();
114
+ if (message.hasGenerationId()) body.generation_id = message.getGenerationId();
115
+ if (message.hasMaxInputs()) body.max_inputs = message.getMaxInputs();
116
+ if (message.hasMaxCharacters()) body.max_characters = message.getMaxCharacters();
117
+ return { body, idempotencyKey: message.hasIdempotencyKey() ? message.getIdempotencyKey() : undefined };
118
+ }
119
+
120
+ export function encodeSubmitRequest(body, idempotencyKey) {
121
+ const message = new knowledgePb.SubmitJobRequest();
122
+ if (body?.repository !== undefined) message.setRepository(body.repository);
123
+ if (body?.operation !== undefined) message.setOperation(operationToProto[body.operation] ?? knowledgePb.JobOperation.JOB_OPERATION_UNSPECIFIED);
124
+ if (body?.scope !== undefined) message.setScope(body.scope);
125
+ if (body?.include !== undefined) {
126
+ const list = new knowledgePb.StringList();
127
+ list.setValuesList(body.include);
128
+ message.setInclude(list);
129
+ }
130
+ if (body?.exclude !== undefined) {
131
+ const list = new knowledgePb.StringList();
132
+ list.setValuesList(body.exclude);
133
+ message.setExclude(list);
134
+ }
135
+ if (body?.embed !== undefined) message.setEmbed(body.embed);
136
+ if (body?.semantic !== undefined) message.setSemantic(body.semantic);
137
+ if (body?.query !== undefined) message.setQuery(body.query);
138
+ if (body?.top_k !== undefined) message.setTopK(body.top_k);
139
+ if (body?.token_budget !== undefined) message.setTokenBudget(body.token_budget);
140
+ if (body?.generation_id !== undefined) message.setGenerationId(body.generation_id);
141
+ if (body?.max_inputs !== undefined) message.setMaxInputs(body.max_inputs);
142
+ if (body?.max_characters !== undefined) message.setMaxCharacters(body.max_characters);
143
+ if (idempotencyKey !== undefined) message.setIdempotencyKey(idempotencyKey);
144
+ return message;
145
+ }
146
+
147
+ export function encodeGetJobRequest(id, RequestType = knowledgePb.GetJobRequest) {
148
+ const request = new RequestType();
149
+ request.setJobId(jobId(id));
150
+ return request;
151
+ }
152
+ export function decodeJobId(request) { return request.hasJobId() ? request.getJobId().getValue() : ''; }
153
+
154
+ export function encodeJobEvent(job, sequence) {
155
+ const event = new knowledgePb.JobEvent();
156
+ event.setJob(encodeJob(job));
157
+ event.setObservedAt(timestamp(new Date().toISOString()));
158
+ event.setSequence(sequence);
159
+ return event;
160
+ }
161
+ export function decodeJobEvent(event) {
162
+ return { job: decodeJob(event.getJob()), observed_at: iso(event.getObservedAt()), sequence: event.getSequence() };
163
+ }
164
+
165
+ export function metadataForToken(token) {
166
+ if (!token) throw new Error('A service token is required.');
167
+ const metadata = new grpc.Metadata();
168
+ metadata.set('authorization', `Bearer ${token}`);
169
+ return metadata;
170
+ }
171
+ export function authorizationFromMetadata(metadata) {
172
+ const values = metadata?.get('authorization') || [];
173
+ return values.find(entry => typeof entry === 'string') || '';
174
+ }
175
+
176
+ export function toGrpcError(error) {
177
+ const safeMessage = error?.status ? error.message : 'Service request failed.';
178
+ const legacyCode = error?.code === 'CONFLICT' ? ERROR_CODE.ABORTED : error?.code;
179
+ const code = Object.values(ERROR_CODE).includes(legacyCode) ? legacyCode : canonicalCodeFromHttpStatus(error?.status);
180
+ const reason = error?.reason || (code === ERROR_CODE.UNAUTHENTICATED ? ERROR_REASON.AUTH_INVALID
181
+ : code === ERROR_CODE.PERMISSION_DENIED ? ERROR_REASON.PERMISSION_DENIED
182
+ : code === ERROR_CODE.NOT_FOUND ? ERROR_REASON.TARGET_NOT_FOUND
183
+ : code === ERROR_CODE.ABORTED ? ERROR_REASON.CONFLICT
184
+ : code === ERROR_CODE.RESOURCE_EXHAUSTED ? ERROR_REASON.CAPACITY_EXHAUSTED
185
+ : code === ERROR_CODE.UNAVAILABLE ? ERROR_REASON.PROVIDER_UNAVAILABLE
186
+ : code === ERROR_CODE.INVALID_ARGUMENT ? ERROR_REASON.INPUT_INVALID
187
+ : ERROR_REASON.INTERNAL);
188
+ const envelope = createErrorEnvelope({
189
+ code,
190
+ reason,
191
+ message: safeMessage,
192
+ retryable: error?.retryable ?? (error?.status === 429 || error?.status === 503),
193
+ retry_after_ms: error?.retry_after_ms ?? null,
194
+ http_status: error?.status ?? defaultHttpStatusForCode(code),
195
+ provider: error?.provider ?? null,
196
+ provider_code: error?.provider_code ?? null,
197
+ });
198
+ const detail = new errorsPb.ErrorDetail();
199
+ detail.setCode(protoErrorCode(envelope.code));
200
+ detail.setMessage(envelope.message);
201
+ detail.setRetryable(envelope.retryable);
202
+ detail.setReason(envelope.reason);
203
+ if (envelope.http_status != null) detail.setHttpStatus(envelope.http_status);
204
+ if (envelope.retry_after_ms != null) detail.setRetryAfterMs(envelope.retry_after_ms);
205
+ if (envelope.provider) detail.setProvider(envelope.provider);
206
+ if (envelope.provider_code) detail.setProviderCode(envelope.provider_code);
207
+ const metadata = new grpc.Metadata();
208
+ metadata.set('agentsam-error-bin', Buffer.from(detail.serializeBinary()));
209
+ return Object.assign(new Error(safeMessage), {
210
+ code: grpcStatusForCode(envelope.code),
211
+ details: safeMessage,
212
+ metadata,
213
+ });
214
+ }
215
+
216
+ export function fromGrpcError(error) {
217
+ let detail = null;
218
+ try {
219
+ const values = error?.metadata?.get('agentsam-error-bin') || [];
220
+ const binary = values.find(value => Buffer.isBuffer(value));
221
+ if (binary) detail = errorsPb.ErrorDetail.deserializeBinary(new Uint8Array(binary));
222
+ } catch { /* fall back to gRPC status */ }
223
+ const message = detail?.getMessage() || error?.details || error?.message || 'Knowledge RPC failed.';
224
+ const rpcCode = detail ? errorCodeNames.get(detail.getCode()) || 'UNKNOWN' : undefined;
225
+ return Object.assign(new Error(message), {
226
+ status: detail?.hasHttpStatus?.() ? detail.getHttpStatus() : grpcToHttp.get(error?.code) ?? 500,
227
+ grpcCode: error?.code,
228
+ rpcCode,
229
+ reason: detail?.getReason?.() || ERROR_REASON.UNKNOWN,
230
+ retryable: detail?.getRetryable() || false,
231
+ retry_after_ms: detail?.hasRetryAfterMs?.() ? detail.getRetryAfterMs() : null,
232
+ provider: detail?.hasProvider?.() ? detail.getProvider() : null,
233
+ provider_code: detail?.hasProviderCode?.() ? detail.getProviderCode() : null,
234
+ });
235
+ }
236
+
237
+ export function isTerminalJob(job) { return job.status === 'completed' || job.status === 'failed'; }
@@ -0,0 +1,83 @@
1
+ import { createBearerTokenVerifier } from './auth.js';
2
+ import { serviceError } from './job-engine.js';
3
+ import {
4
+ grpc,
5
+ KnowledgeServiceService,
6
+ authorizationFromMetadata,
7
+ decodeJobId,
8
+ decodeSubmitRequest,
9
+ encodeJob,
10
+ encodeJobEvent,
11
+ encodeRepositoryList,
12
+ isTerminalJob,
13
+ toGrpcError,
14
+ } from './grpc-codec.js';
15
+
16
+ export async function startKnowledgeGrpcServer({
17
+ engine,
18
+ token,
19
+ verifyToken,
20
+ port = 0,
21
+ host = '127.0.0.1',
22
+ credentials = grpc.ServerCredentials.createInsecure(),
23
+ } = {}) {
24
+ if (!engine) throw new Error('Knowledge job engine is required.');
25
+ const verify = verifyToken || createBearerTokenVerifier(token);
26
+ const authenticate = call => {
27
+ if (!verify(authorizationFromMetadata(call.metadata))) throw serviceError(401, 'Unauthorized.', 'UNAUTHENTICATED');
28
+ };
29
+ const unary = handler => (call, callback) => {
30
+ try {
31
+ authenticate(call);
32
+ callback(null, handler(call));
33
+ } catch (error) {
34
+ callback(toGrpcError(error));
35
+ }
36
+ };
37
+
38
+ const server = new grpc.Server();
39
+ server.addService(KnowledgeServiceService, {
40
+ listRepositories: unary(() => encodeRepositoryList(engine.listRepositories())),
41
+ submitJob: unary(call => {
42
+ const { body, idempotencyKey } = decodeSubmitRequest(call.request);
43
+ return encodeJob(engine.submitJob(body, { idempotencyKey }).job);
44
+ }),
45
+ getJob: unary(call => encodeJob(engine.getJob(decodeJobId(call.request)))),
46
+ watchJob(call) {
47
+ let unsubscribe = () => {};
48
+ let ended = false;
49
+ try {
50
+ authenticate(call);
51
+ let sequence = 0;
52
+ unsubscribe = engine.watchJob(decodeJobId(call.request), job => {
53
+ if (ended || call.cancelled) return;
54
+ call.write(encodeJobEvent(job, ++sequence));
55
+ if (isTerminalJob(job)) {
56
+ ended = true;
57
+ queueMicrotask(() => {
58
+ unsubscribe();
59
+ if (!call.cancelled) call.end();
60
+ });
61
+ }
62
+ });
63
+ call.once('cancelled', () => { ended = true; unsubscribe(); });
64
+ call.once('close', () => { ended = true; unsubscribe(); });
65
+ } catch (error) {
66
+ ended = true;
67
+ unsubscribe();
68
+ call.emit('error', toGrpcError(error));
69
+ }
70
+ },
71
+ });
72
+
73
+ const boundPort = await new Promise((resolve, reject) => {
74
+ server.bindAsync(`${host}:${port}`, credentials, (error, actualPort) => error ? reject(error) : resolve(actualPort));
75
+ });
76
+ return {
77
+ server,
78
+ address: { host, port: boundPort },
79
+ target: `${host}:${boundPort}`,
80
+ async close() { await new Promise(resolve => server.tryShutdown(resolve)); },
81
+ forceClose() { server.forceShutdown(); },
82
+ };
83
+ }
@@ -0,0 +1,248 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fork } from 'node:child_process';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { fingerprint, validateConfig } from '../config.js';
8
+
9
+ const defaultWorkerPath = fileURLToPath(new URL('./job-worker.js', import.meta.url));
10
+ const TERMINAL_STATUSES = new Set(['completed', 'failed']);
11
+
12
+ export function serviceError(status, message, code) {
13
+ return Object.assign(new Error(message), { status, code });
14
+ }
15
+
16
+ function decode(row) {
17
+ return row && {
18
+ id: row.id,
19
+ status: row.status,
20
+ attempts: row.attempts,
21
+ created_at: row.created_at,
22
+ updated_at: row.updated_at,
23
+ result: row.result ? JSON.parse(row.result) : null,
24
+ error: row.error,
25
+ };
26
+ }
27
+
28
+ function normalizeRepositories(repositories) {
29
+ if (!repositories || !Object.keys(repositories).length) throw new Error('Register at least one repository.');
30
+ return Object.fromEntries(Object.entries(repositories).map(([name, repo]) => {
31
+ if (!/^[a-z][a-z0-9_-]{0,47}$/.test(name)) throw new Error('Invalid repository alias.');
32
+ const root = fs.realpathSync(repo.root);
33
+ if (!fs.statSync(root).isDirectory()) throw new Error('Repository root must be a directory.');
34
+ const config = validateConfig(repo.config);
35
+ if (config.storage.driver !== 'sqlite') throw new Error('This service release uses its durable SQLite volume; Postgres is not configured by this preset.');
36
+ return [name, { root, config }];
37
+ }));
38
+ }
39
+
40
+ function normalizeRequest(body, repositories, allowEmbeddings) {
41
+ if (!body || typeof body !== 'object' || Array.isArray(body)) throw serviceError(400, 'Expected a JSON object.', 'INVALID_ARGUMENT');
42
+ const keys = ['repository', 'operation', 'scope', 'include', 'exclude', 'embed', 'semantic', 'query', 'top_k', 'token_budget', 'generation_id', 'max_inputs', 'max_characters'];
43
+ if (Object.keys(body).some(k => !keys.includes(k))) throw serviceError(400, 'Unknown job field.', 'INVALID_ARGUMENT');
44
+ const repo = Object.hasOwn(repositories, body.repository) && repositories[body.repository];
45
+ if (!repo) throw serviceError(404, 'Repository is not registered.', 'NOT_FOUND');
46
+ if (!['index', 'plan', 'search'].includes(body.operation)) throw serviceError(400, 'operation must be index, plan, or search.', 'INVALID_ARGUMENT');
47
+ for (const key of ['embed', 'semantic']) {
48
+ if (body[key] !== undefined && typeof body[key] !== 'boolean') throw serviceError(400, `${key} must be a boolean.`, 'INVALID_ARGUMENT');
49
+ }
50
+ if (((body.embed && body.operation !== 'plan') || body.semantic) && !allowEmbeddings) {
51
+ throw serviceError(403, 'Embedding calls are disabled on this service.', 'PERMISSION_DENIED');
52
+ }
53
+ if (body.exclude !== undefined && !Array.isArray(body.exclude)) throw serviceError(400, 'exclude must be an array.', 'INVALID_ARGUMENT');
54
+ if (body.operation === 'search' && (typeof body.query !== 'string' || !body.query.trim() || body.query.length > 8000)) {
55
+ throw serviceError(400, 'A query of 1..8000 characters is required.', 'INVALID_ARGUMENT');
56
+ }
57
+ const bounded = (key, fallback, min, max) => {
58
+ const value = body[key] ?? fallback;
59
+ if (!Number.isInteger(value) || value < min || value > max) throw serviceError(400, `${key} must be ${min}..${max}.`, 'INVALID_ARGUMENT');
60
+ return value;
61
+ };
62
+ const request = {
63
+ ...body,
64
+ embed: body.embed ?? false,
65
+ semantic: body.semantic ?? false,
66
+ max_inputs: bounded('max_inputs', 100, 0, 1000),
67
+ max_characters: bounded('max_characters', 200000, 0, 2000000),
68
+ top_k: bounded('top_k', 8, 1, 8),
69
+ token_budget: bounded('token_budget', 6000, 256, 6000),
70
+ };
71
+ const config = structuredClone(repo.config);
72
+ if (body.scope !== undefined) config.scope.name = body.scope;
73
+ if (body.include !== undefined) config.scope.include = body.include;
74
+ if (body.exclude !== undefined) config.scope.exclude = [...config.scope.exclude, ...body.exclude];
75
+ let checked;
76
+ try {
77
+ checked = validateConfig(config);
78
+ } catch (error) {
79
+ throw serviceError(400, error.message, 'INVALID_ARGUMENT');
80
+ }
81
+ if (checked.scope.include.some(p => !repo.config.scope.include.some(allowed => allowed === '.' || p === allowed || p.startsWith(allowed + '/')))) {
82
+ throw serviceError(403, 'Requested include is outside the registered scope.', 'PERMISSION_DENIED');
83
+ }
84
+ return { request, config: checked, root: repo.root };
85
+ }
86
+
87
+ export function createKnowledgeJobEngine({
88
+ stateDir,
89
+ repositories,
90
+ allowEmbeddings = false,
91
+ maxQueued = 32,
92
+ maxFiles = 2000,
93
+ jobTimeoutMs = 600000,
94
+ workerPath = defaultWorkerPath,
95
+ } = {}) {
96
+ const registry = normalizeRepositories(repositories);
97
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
98
+ const jobsFile = path.join(stateDir, 'jobs.sqlite');
99
+ const db = new DatabaseSync(jobsFile);
100
+ db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;
101
+ CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, idem TEXT UNIQUE, digest TEXT NOT NULL,
102
+ payload TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0,
103
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, result TEXT, error TEXT);
104
+ CREATE INDEX IF NOT EXISTS jobs_pending ON jobs(status, created_at);`);
105
+ fs.chmodSync(jobsFile, 0o600);
106
+ db.prepare("UPDATE jobs SET status=CASE WHEN attempts>=3 THEN 'failed' ELSE 'queued' END, error=CASE WHEN attempts>=3 THEN 'Interrupted three times; submit a new job after investigation.' ELSE NULL END WHERE status='running'").run();
107
+
108
+ let child = null;
109
+ let closing = false;
110
+ let closed = false;
111
+ let pumping = false;
112
+ const watchers = new Map();
113
+
114
+ const getRaw = id => decode(db.prepare('SELECT * FROM jobs WHERE id=?').get(id));
115
+ const publish = job => {
116
+ const listeners = watchers.get(job.id);
117
+ if (!listeners) return;
118
+ for (const listener of [...listeners]) {
119
+ try { listener(job); } catch { /* transport observers cannot break durable state transitions */ }
120
+ }
121
+ if (TERMINAL_STATUSES.has(job.status)) watchers.delete(job.id);
122
+ };
123
+ const updateStatus = (id, status, { result = null, error = null } = {}) => {
124
+ db.prepare('UPDATE jobs SET status=?,result=?,error=?,updated_at=? WHERE id=?').run(
125
+ status,
126
+ result === null ? null : JSON.stringify(result),
127
+ error,
128
+ new Date().toISOString(),
129
+ id,
130
+ );
131
+ const job = getRaw(id);
132
+ publish(job);
133
+ return job;
134
+ };
135
+
136
+ const pump = () => {
137
+ if (closing || pumping) return;
138
+ const row = db.prepare("SELECT * FROM jobs WHERE status='queued' ORDER BY created_at,rowid LIMIT 1").get();
139
+ if (!row) return;
140
+ pumping = true;
141
+ db.prepare("UPDATE jobs SET status='running',attempts=attempts+1,updated_at=? WHERE id=?").run(new Date().toISOString(), row.id);
142
+ publish(getRaw(row.id));
143
+ let response = null;
144
+ let timedOut = false;
145
+ child = fork(workerPath, [], { execArgv: [], stdio: ['ignore', 'ignore', 'ignore', 'ipc'], env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } });
146
+ const timer = setTimeout(() => { timedOut = true; child?.kill('SIGKILL'); }, jobTimeoutMs);
147
+ child.once('message', value => { response = value; });
148
+ child.once('error', () => { response = { ok: false, error: 'Could not start indexing process.' }; });
149
+ child.once('close', () => {
150
+ clearTimeout(timer);
151
+ child = null;
152
+ if (!closing) {
153
+ const ok = response?.ok && !timedOut;
154
+ updateStatus(row.id, ok ? 'completed' : 'failed', {
155
+ result: ok ? response.result : null,
156
+ error: ok ? null : timedOut ? 'Job exceeded its time limit; narrow the scope.' : response?.error || 'Indexing process exited unexpectedly.',
157
+ });
158
+ }
159
+ pumping = false;
160
+ if (!closing) setImmediate(pump);
161
+ });
162
+ const payload = JSON.parse(row.payload);
163
+ const registered = registry[payload.request.repository];
164
+ if (!registered || registered.config.repository_id !== payload.config.repository_id || fingerprint(registered.config.scope) !== payload.registered_scope || (!allowEmbeddings && ((payload.request.embed && payload.request.operation !== 'plan') || payload.request.semantic))) {
165
+ response = { ok: false, error: 'Repository registration changed; resubmit this job.' };
166
+ child.kill();
167
+ return;
168
+ }
169
+ child.send({ ...payload, root: registered.root, filename: path.join(stateDir, 'knowledge.sqlite'), maxFiles });
170
+ };
171
+
172
+ const api = {
173
+ listRepositories() {
174
+ return {
175
+ repositories: Object.entries(registry).map(([alias, repo]) => ({ alias, repository_id: repo.config.repository_id })),
176
+ embeddings_enabled: allowEmbeddings,
177
+ };
178
+ },
179
+ getJob(id) {
180
+ if (typeof id !== 'string' || !/^[a-f0-9-]{36}$/.test(id)) throw serviceError(400, 'Invalid job id.', 'INVALID_ARGUMENT');
181
+ const job = getRaw(id);
182
+ if (!job) throw serviceError(404, 'Job not found.', 'NOT_FOUND');
183
+ return job;
184
+ },
185
+ submitJob(body, { idempotencyKey } = {}) {
186
+ if (closing) throw serviceError(503, 'Service is stopping.', 'UNAVAILABLE');
187
+ const payload = normalizeRequest(body, registry, allowEmbeddings);
188
+ payload.registered_scope = fingerprint(registry[payload.request.repository].config.scope);
189
+ if (idempotencyKey !== undefined && (typeof idempotencyKey !== 'string' || !/^[\w:.-]{1,128}$/.test(idempotencyKey))) {
190
+ throw serviceError(400, 'Invalid Idempotency-Key.', 'INVALID_ARGUMENT');
191
+ }
192
+ const digest = fingerprint(payload);
193
+ const idem = idempotencyKey ? fingerprint([payload.config.repository_id, payload.registered_scope, idempotencyKey]) : null;
194
+ const previous = idem && db.prepare('SELECT * FROM jobs WHERE idem=?').get(idem);
195
+ if (previous) {
196
+ if (previous.digest !== digest) throw serviceError(409, 'Idempotency-Key already used for a different job.', 'CONFLICT');
197
+ return { job: decode(previous), created: false };
198
+ }
199
+ if (db.prepare("SELECT COUNT(*) AS n FROM jobs WHERE status IN ('queued','running')").get().n >= maxQueued) {
200
+ throw serviceError(429, 'Job queue is full; retry later with the same Idempotency-Key.', 'RESOURCE_EXHAUSTED');
201
+ }
202
+ const id = randomUUID();
203
+ const now = new Date().toISOString();
204
+ db.prepare("INSERT INTO jobs(id,idem,digest,payload,status,created_at,updated_at) VALUES(?,?,?,?,'queued',?,?)").run(
205
+ id,
206
+ idem,
207
+ digest,
208
+ JSON.stringify(payload),
209
+ now,
210
+ now,
211
+ );
212
+ const job = getRaw(id);
213
+ publish(job);
214
+ setImmediate(pump);
215
+ return { job, created: true };
216
+ },
217
+ watchJob(id, listener) {
218
+ if (typeof listener !== 'function') throw new TypeError('watchJob listener must be a function.');
219
+ const current = api.getJob(id);
220
+ listener(current);
221
+ if (TERMINAL_STATUSES.has(current.status)) return () => {};
222
+ let listeners = watchers.get(id);
223
+ if (!listeners) watchers.set(id, listeners = new Set());
224
+ listeners.add(listener);
225
+ return () => {
226
+ listeners.delete(listener);
227
+ if (!listeners.size) watchers.delete(id);
228
+ };
229
+ },
230
+ isClosing() {
231
+ return closing;
232
+ },
233
+ async close() {
234
+ if (closed) return;
235
+ closing = true;
236
+ watchers.clear();
237
+ if (child) {
238
+ const active = child;
239
+ await new Promise(resolve => { active.once('close', resolve); active.kill('SIGKILL'); });
240
+ }
241
+ db.close();
242
+ closed = true;
243
+ },
244
+ };
245
+
246
+ setImmediate(pump);
247
+ return api;
248
+ }