@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.
- package/AGENTSAM.md +55 -0
- package/README.md +12 -8
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/CLI_SHELL.md +163 -53
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +16 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +45 -12
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/contracts/auth-config.js +18 -7
- package/packages/identity/tests/auth-config.test.mjs +9 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +47 -0
- package/protocol/context/context-budget.schema.json +10 -15
- package/protocol/context/context-item.schema.json +4 -5
- package/protocol/context/resolved-context-pack.schema.json +19 -14
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/catalog.json +18 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +353 -0
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +118 -31
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +129 -0
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/deploy.js +39 -3
- package/src/commands/env.js +90 -0
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +2 -5
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +149 -46
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +130 -61
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +5 -3
- package/src/commands/shell.js +568 -119
- package/src/commands/tunnel.js +2 -2
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +68 -6
- package/src/context/index.js +3 -1
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +44 -12
- package/src/errors/contract.js +236 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +23 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +68 -2
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/cli-preferences.js +55 -24
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/detect-context.js +2 -2
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +148 -0
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +183 -0
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +23 -16
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/catalog.js +135 -0
- package/src/models/discovery.js +292 -0
- package/src/models/index.js +7 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +10 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +426 -0
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/process.js +35 -9
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +51 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +1 -0
- package/src/ui/boot.js +15 -17
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/account-session.test.mjs +36 -0
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/cli-preferences.test.mjs +26 -5
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +61 -12
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +115 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +48 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +127 -16
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +21 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +96 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +150 -0
- package/test/shell.test.mjs +92 -23
- package/test/smoke.mjs +4 -1
- package/test/telemetry.test.mjs +79 -0
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- package/test/tools-search.test.mjs +14 -1
- package/test/whoami-resume.test.mjs +56 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const DEFAULT_MAX_DETAIL_CHARS = 12_000;
|
|
2
|
+
const SECRET_KEY = /(?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|password|secret|cookie|credential)/i;
|
|
3
|
+
const SECRET_VALUE_PATTERNS = [
|
|
4
|
+
/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
|
|
5
|
+
/\bsk-[A-Za-z0-9_-]{12,}\b/g,
|
|
6
|
+
/\bsdk_[A-Za-z0-9_-]{8,}\b/g,
|
|
7
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
11
|
+
function bounded(value, maxChars = DEFAULT_MAX_DETAIL_CHARS) {
|
|
12
|
+
const text = String(value ?? '');
|
|
13
|
+
if (text.length <= maxChars) return text;
|
|
14
|
+
return `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function redactString(value, maxChars) {
|
|
18
|
+
let text = bounded(value, maxChars);
|
|
19
|
+
for (const pattern of SECRET_VALUE_PATTERNS) text = text.replace(pattern, '[REDACTED]');
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function redactDiagnosticValue(value, options = {}, depth = 0, key = '') {
|
|
24
|
+
const maxChars = Number.isInteger(options.maxChars) && options.maxChars > 0 ? options.maxChars : DEFAULT_MAX_DETAIL_CHARS;
|
|
25
|
+
if (SECRET_KEY.test(key)) return '[REDACTED]';
|
|
26
|
+
if (depth > 6) return '[TRUNCATED_DEPTH]';
|
|
27
|
+
if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
28
|
+
if (typeof value === 'string') return redactString(value, maxChars);
|
|
29
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((item) => redactDiagnosticValue(item, options, depth + 1));
|
|
30
|
+
if (typeof value === 'object') {
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const [childKey, childValue] of Object.entries(value).slice(0, 80)) {
|
|
33
|
+
result[childKey] = redactDiagnosticValue(childValue, options, depth + 1, childKey);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
return redactString(String(value), maxChars);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function header(headers, name) {
|
|
41
|
+
if (!headers) return '';
|
|
42
|
+
if (typeof headers.get === 'function') return clean(headers.get(name));
|
|
43
|
+
const wanted = name.toLowerCase();
|
|
44
|
+
for (const [key, value] of Object.entries(headers)) if (String(key).toLowerCase() === wanted) return clean(value);
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function retryAfterMs(headers) {
|
|
49
|
+
const value = header(headers, 'retry-after');
|
|
50
|
+
if (!value) return null;
|
|
51
|
+
if (/^\d+(?:\.\d+)?$/.test(value)) return Math.max(0, Math.round(Number(value) * 1000));
|
|
52
|
+
const when = Date.parse(value);
|
|
53
|
+
return Number.isFinite(when) ? Math.max(0, when - Date.now()) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const OPENAI_NON_RETRY_CODES = new Set([
|
|
57
|
+
'credit_balance_exhausted',
|
|
58
|
+
'organization_spend_limit_exceeded',
|
|
59
|
+
'project_spend_limit_exceeded',
|
|
60
|
+
'organization_usage_limit_exceeded',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export function classifyOpenAIError({ status, type, code, message, param } = {}) {
|
|
64
|
+
const httpStatus = Number(status || 0);
|
|
65
|
+
const normalizedType = clean(type).toLowerCase();
|
|
66
|
+
const normalizedCode = clean(code).toLowerCase();
|
|
67
|
+
const diagnosticText = `${normalizedCode} ${normalizedType} ${clean(param).toLowerCase()} ${clean(message).toLowerCase()}`;
|
|
68
|
+
if (normalizedCode === 'previous_response_not_found') return { category: 'continuation', retriable: true, retry_strategy: 'retry_full_context' };
|
|
69
|
+
if (normalizedCode === 'websocket_connection_limit_reached') return { category: 'connection_lifetime', retriable: true, retry_strategy: 'reconnect' };
|
|
70
|
+
if (OPENAI_NON_RETRY_CODES.has(normalizedCode)) return { category: 'billing_or_quota', retriable: false, retry_strategy: 'operator_action' };
|
|
71
|
+
if (httpStatus === 400 && /service[_ -]?tier/.test(diagnosticText)) return { category: 'service_tier', retriable: false, retry_strategy: 'change_configuration' };
|
|
72
|
+
if (httpStatus === 400) return { category: 'invalid_request', retriable: false, retry_strategy: 'change_request' };
|
|
73
|
+
if (httpStatus === 401) return { category: 'authentication', retriable: false, retry_strategy: 'fix_credentials' };
|
|
74
|
+
if (httpStatus === 403) return { category: 'authorization_or_region', retriable: false, retry_strategy: 'operator_action' };
|
|
75
|
+
if (httpStatus === 429 && normalizedCode === 'slow_down') return { category: 'ramp_rate', retriable: true, retry_strategy: 'retry_after_backoff' };
|
|
76
|
+
if (httpStatus === 429) return { category: 'rate_limit', retriable: true, retry_strategy: 'retry_after_backoff' };
|
|
77
|
+
if (httpStatus === 500) return { category: 'provider_server', retriable: true, retry_strategy: 'retry_backoff' };
|
|
78
|
+
if (httpStatus === 503 || normalizedCode === 'server_is_overloaded') return { category: 'provider_overload', retriable: true, retry_strategy: 'retry_after_backoff' };
|
|
79
|
+
return { category: 'provider_error', retriable: false, retry_strategy: 'inspect_error' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class AgentSamDiagnosticError extends Error {
|
|
83
|
+
constructor(diagnostic, options = {}) {
|
|
84
|
+
super(clean(diagnostic?.message) || clean(options.message) || 'AgentSam operation failed', options.cause ? { cause: options.cause } : undefined);
|
|
85
|
+
this.name = 'AgentSamDiagnosticError';
|
|
86
|
+
this.diagnostic = Object.freeze({ schema_version: 1, ...diagnostic });
|
|
87
|
+
this.code = diagnostic?.code || diagnostic?.kind || 'agentsam_error';
|
|
88
|
+
this.status = diagnostic?.http_status ?? diagnostic?.status ?? null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createOpenAIHttpError({ status, body, headers, requestedServiceTier, rawText } = {}) {
|
|
93
|
+
const payload = body && typeof body === 'object' ? body : {};
|
|
94
|
+
const providerError = payload.error && typeof payload.error === 'object' ? payload.error : payload;
|
|
95
|
+
const type = clean(providerError.type) || null;
|
|
96
|
+
const code = clean(providerError.code) || null;
|
|
97
|
+
const message = clean(providerError.message || payload.message || rawText) || `OpenAI API returned HTTP ${status}`;
|
|
98
|
+
const classification = classifyOpenAIError({ status, type, code, message, param: providerError.param });
|
|
99
|
+
const diagnostic = {
|
|
100
|
+
source: 'openai',
|
|
101
|
+
kind: 'provider_http_error',
|
|
102
|
+
http_status: Number(status || 0) || null,
|
|
103
|
+
type,
|
|
104
|
+
code,
|
|
105
|
+
param: clean(providerError.param) || null,
|
|
106
|
+
message: redactString(message, 4_000),
|
|
107
|
+
category: classification.category,
|
|
108
|
+
retriable: classification.retriable,
|
|
109
|
+
retry_strategy: classification.retry_strategy,
|
|
110
|
+
retry_after_ms: retryAfterMs(headers),
|
|
111
|
+
request_id: header(headers, 'x-request-id') || header(headers, 'openai-request-id') || header(headers, 'request-id') || null,
|
|
112
|
+
ray_id: header(headers, 'cf-ray') || null,
|
|
113
|
+
requested_service_tier: clean(requestedServiceTier) || null,
|
|
114
|
+
details: redactDiagnosticValue(payload, { maxChars: DEFAULT_MAX_DETAIL_CHARS }),
|
|
115
|
+
};
|
|
116
|
+
return new AgentSamDiagnosticError(diagnostic);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createProcessDiagnosticError({ code, message, command, args, cwd, exitCode, signal, stderr, stdout, cause, retriable = false } = {}) {
|
|
120
|
+
return new AgentSamDiagnosticError({
|
|
121
|
+
source: 'process',
|
|
122
|
+
kind: 'process_error',
|
|
123
|
+
code: clean(code) || 'process_failed',
|
|
124
|
+
message: clean(message) || 'Process failed',
|
|
125
|
+
retriable: Boolean(retriable),
|
|
126
|
+
retry_strategy: retriable ? 'retry_after_inspection' : 'inspect_error',
|
|
127
|
+
command: clean(command) || null,
|
|
128
|
+
args: Array.isArray(args) ? args.slice(0, 64).map((value) => redactString(value, 1_000)) : [],
|
|
129
|
+
cwd: clean(cwd) || null,
|
|
130
|
+
exit_code: Number.isInteger(exitCode) ? exitCode : null,
|
|
131
|
+
signal: clean(signal) || null,
|
|
132
|
+
stderr: redactString(stderr || '', 12_000) || null,
|
|
133
|
+
stdout: redactString(stdout || '', 4_000) || null,
|
|
134
|
+
}, { cause });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function diagnosticFromError(error, fallback = {}) {
|
|
138
|
+
if (error?.diagnostic) return error.diagnostic;
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
schema_version: 1,
|
|
141
|
+
source: fallback.source || 'agentsam',
|
|
142
|
+
kind: fallback.kind || 'runtime_error',
|
|
143
|
+
code: clean(error?.code || fallback.code) || 'runtime_error',
|
|
144
|
+
message: redactString(error?.message || error || fallback.message || 'Unknown error', 4_000),
|
|
145
|
+
http_status: Number(error?.status || fallback.status || 0) || null,
|
|
146
|
+
retriable: Boolean(fallback.retriable),
|
|
147
|
+
retry_strategy: fallback.retry_strategy || 'inspect_error',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function renderDiagnosticError(error) {
|
|
152
|
+
const d = diagnosticFromError(error);
|
|
153
|
+
const identity = [d.source, d.http_status ? `HTTP ${d.http_status}` : '', d.type, d.code].filter(Boolean).join(' · ');
|
|
154
|
+
const lines = [`✗ ${d.message}`, identity ? ` ${identity}` : ''];
|
|
155
|
+
if (d.request_id) lines.push(` request_id: ${d.request_id}`);
|
|
156
|
+
if (d.ray_id) lines.push(` ray_id: ${d.ray_id}`);
|
|
157
|
+
if (d.retry_after_ms != null) lines.push(` retry_after_ms: ${d.retry_after_ms}`);
|
|
158
|
+
if (d.retry_strategy) lines.push(` retry: ${d.retriable ? 'yes' : 'no'} · ${d.retry_strategy}`);
|
|
159
|
+
return lines.filter(Boolean).join('\n');
|
|
160
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ERROR_SCHEMA_VERSION,
|
|
3
|
+
ERROR_CODE,
|
|
4
|
+
ERROR_REASON,
|
|
5
|
+
GRPC_STATUS,
|
|
6
|
+
canonicalCodeFromHttpStatus,
|
|
7
|
+
canonicalCodeFromGrpcStatus,
|
|
8
|
+
defaultHttpStatusForCode,
|
|
9
|
+
grpcStatusForCode,
|
|
10
|
+
classifyCloudflareFailure,
|
|
11
|
+
classifyOAuthFailure,
|
|
12
|
+
createErrorEnvelope,
|
|
13
|
+
} from './contract.js';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
AgentSamDiagnosticError,
|
|
17
|
+
classifyOpenAIError,
|
|
18
|
+
createOpenAIHttpError,
|
|
19
|
+
createProcessDiagnosticError,
|
|
20
|
+
diagnosticFromError,
|
|
21
|
+
redactDiagnosticValue,
|
|
22
|
+
renderDiagnosticError,
|
|
23
|
+
} from './diagnostic.js';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { assessContextUsage, compactContextItem, createContextBudget, estimateContextTokens, rehydrateContextRef, resolveContext } from '../context/index.js';
|
|
2
|
+
import { calculateModelCost, getModelRecord } from '../models/index.js';
|
|
3
|
+
import { hydrateToolSchemas, searchToolCards } from '../tools/index.js';
|
|
4
|
+
|
|
5
|
+
const STRATEGIES = Object.freeze(['bounded', 'discovery', 'compact']);
|
|
6
|
+
|
|
7
|
+
const BASE_TOOLS = Object.freeze([
|
|
8
|
+
{ name: 'repository.snapshot', description: 'Inspect repository identity, tree, packages, and structural evidence.', category: 'repository', input_schema: { type: 'object', properties: { cwd: { type: 'string' } } } },
|
|
9
|
+
{ name: 'code.symbols', description: 'Find exact symbols, declarations, imports, and callers in indexed source.', category: 'code', input_schema: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } } },
|
|
10
|
+
{ name: 'files.read', description: 'Read a selected file or bounded source range by stable reference.', category: 'files', input_schema: { type: 'object', required: ['ref'], properties: { ref: { type: 'string' } } } },
|
|
11
|
+
{ name: 'tests.trace', description: 'Trace a failing test to referenced source symbols and files.', category: 'tests', input_schema: { type: 'object', required: ['test'], properties: { test: { type: 'string' } } } },
|
|
12
|
+
{ name: 'context.rehydrate', description: 'Recover previously compacted evidence by stable reference and hash.', category: 'context', input_schema: { type: 'object', required: ['ref'], properties: { ref: { type: 'string' } } } },
|
|
13
|
+
...Array.from({ length: 20 }, (_, index) => ({ name: `misc.tool.${index}`, description: `Unrelated generic capability ${index}.`, category: 'misc', input_schema: { type: 'object', properties: {} } })),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const FIXTURES = Object.freeze({
|
|
17
|
+
'exact-symbol-callers': {
|
|
18
|
+
task: 'Find the exact resolveContext implementation and its budget dependency before changing context selection.',
|
|
19
|
+
required: ['file:src/context/resolve.js', 'file:src/context/budget.js'],
|
|
20
|
+
syntheticBaseTokens: 8_000,
|
|
21
|
+
items: [
|
|
22
|
+
{ ref: 'file:src/context/resolve.js', kind: 'file', priority: 100, content: 'resolveContext selects evidence, computes receipt composition, and defers overflow refs.'.repeat(40) },
|
|
23
|
+
{ ref: 'file:src/context/budget.js', kind: 'file', priority: 95, content: 'createContextBudget separates model capacity from AgentSam working-set policy.'.repeat(36) },
|
|
24
|
+
{ ref: 'file:README.md', kind: 'file', priority: 8, content: 'General product documentation.'.repeat(100) },
|
|
25
|
+
{ ref: 'file:apps/local-studio/theme.css', kind: 'file', priority: 1, content: 'Unrelated visual styles.'.repeat(160) },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
'two-file-repair': {
|
|
29
|
+
task: 'Repair a shell model-control regression spanning CLI preferences and slash dispatch without loading unrelated apps.',
|
|
30
|
+
required: ['file:src/lib/cli-preferences.js', 'file:src/commands/shell.js'],
|
|
31
|
+
syntheticBaseTokens: 14_000,
|
|
32
|
+
items: [
|
|
33
|
+
{ ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 100, content: 'CLI preference schema and persistence.'.repeat(90) },
|
|
34
|
+
{ ref: 'file:src/commands/shell.js', kind: 'file', priority: 98, content: 'Slash command parsing, runtime cwd, model and context controls.'.repeat(120) },
|
|
35
|
+
{ ref: 'file:apps/cad-creator/frontend/app.tsx', kind: 'file', priority: 5, content: 'CAD UI.'.repeat(300) },
|
|
36
|
+
{ ref: 'file:docs/identity.md', kind: 'file', priority: 4, content: 'Identity docs.'.repeat(240) },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
'config-without-runtime-pollution': {
|
|
40
|
+
task: 'Update portable project defaults while keeping account, run, connection, and execution state out of committed config.',
|
|
41
|
+
required: ['file:.agentsam/config.json', 'file:src/lib/cli-preferences.js'],
|
|
42
|
+
syntheticBaseTokens: 10_000,
|
|
43
|
+
items: [
|
|
44
|
+
{ ref: 'file:.agentsam/config.json', kind: 'file', priority: 100, content: 'Portable repository identity and defaults only.'.repeat(90) },
|
|
45
|
+
{ ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 90, content: 'Gitignored per-machine model, runtime, terminal, and trust preferences.'.repeat(85) },
|
|
46
|
+
{ ref: 'memory:run-history', kind: 'memory', priority: 3, content: 'Large hosted run history must not be copied into project config.'.repeat(500), consumed: true },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
'trace-failing-test': {
|
|
50
|
+
task: 'Trace a failing shell regression test to the exact implementation and fix only the implicated source.',
|
|
51
|
+
required: ['file:test/shell.test.mjs', 'file:src/commands/shell.js'],
|
|
52
|
+
syntheticBaseTokens: 22_000,
|
|
53
|
+
items: [
|
|
54
|
+
{ ref: 'file:test/shell.test.mjs', kind: 'file', priority: 100, content: 'Regression assertions for slash dispatch and runtime cwd.'.repeat(110) },
|
|
55
|
+
{ ref: 'file:src/commands/shell.js', kind: 'file', priority: 96, content: 'Command implementation under test.'.repeat(140) },
|
|
56
|
+
{ ref: 'file:test/cad.test.mjs', kind: 'file', priority: 2, content: 'Unrelated CAD tests.'.repeat(240) },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
'contradictory-paths': {
|
|
60
|
+
task: 'Detect two contradictory model-selection paths and choose one canonical runtime preference path.',
|
|
61
|
+
required: ['file:src/commands/models.js', 'file:src/commands/preferences.js', 'file:src/lib/cli-preferences.js'],
|
|
62
|
+
syntheticBaseTokens: 36_000,
|
|
63
|
+
items: [
|
|
64
|
+
{ ref: 'file:src/commands/models.js', kind: 'file', priority: 100, content: 'Provider model discovery and availability proof.'.repeat(105) },
|
|
65
|
+
{ ref: 'file:src/commands/preferences.js', kind: 'file', priority: 98, content: 'Interactive model, reasoning, and processing selection.'.repeat(100) },
|
|
66
|
+
{ ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 96, content: 'Canonical local preference persistence.'.repeat(90) },
|
|
67
|
+
{ ref: 'file:legacy/model-picker.js', kind: 'file', priority: 15, content: 'Legacy duplicate picker path that should not become a second authority.'.repeat(200), consumed: true },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
'continuation-after-compaction': {
|
|
71
|
+
task: 'Continue a long tool-heavy run by compacting consumed evidence and rehydrating the exact result needed for the next step.',
|
|
72
|
+
required: ['tool:call_previous', 'file:src/context/compact.js'],
|
|
73
|
+
syntheticBaseTokens: 174_000,
|
|
74
|
+
items: [
|
|
75
|
+
{ ref: 'tool:call_previous', kind: 'tool_result', priority: 100, content: 'Critical prior tool evidence with stable identity and the exact invariant required later. '.repeat(900), consumed: true },
|
|
76
|
+
{ ref: 'file:src/context/compact.js', kind: 'file', priority: 95, content: 'Compaction preserves ref hash and source size while shrinking active context.'.repeat(100) },
|
|
77
|
+
{ ref: 'file:unrelated/generated.log', kind: 'file', priority: 1, content: 'Noisy old generated log.'.repeat(500), consumed: true },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function budgetFor(record) {
|
|
83
|
+
const p = record.context_policy;
|
|
84
|
+
return createContextBudget({ windowTokens: record.context_window, targetInputTokens: p.target_input_tokens, compactAtTokens: p.compact_at_tokens, interveneAtTokens: p.intervene_at_tokens, maxNormalInputTokens: p.max_normal_input_tokens, pricingThresholdTokens: p.pricing_threshold_tokens, safetyMarginTokens: p.safety_margin_tokens });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fixture(name) {
|
|
88
|
+
const value = FIXTURES[name];
|
|
89
|
+
if (!value) throw new Error(`unknown context eval fixture: ${name}`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runStrategy(fx, strategy, record) {
|
|
94
|
+
const budget = budgetFor(record);
|
|
95
|
+
let items = fx.items.map(item => ({ ...item }));
|
|
96
|
+
let compactedChars = 0;
|
|
97
|
+
const sources = new Map(items.map(item => [item.ref, item]));
|
|
98
|
+
const rehydratedRefs = [];
|
|
99
|
+
|
|
100
|
+
if (strategy === 'compact') {
|
|
101
|
+
items = items.map(item => {
|
|
102
|
+
if (!item.consumed) return item;
|
|
103
|
+
const compacted = compactContextItem(item, { maxChars: 4_000 });
|
|
104
|
+
compactedChars += Math.max(0, item.content.length - compacted.content.length);
|
|
105
|
+
return compacted;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tools = strategy === 'bounded' ? { cards: [], receipt: { returned_items: 0, chars: 0 } } : searchToolCards(BASE_TOOLS, fx.task, { maxItems: 8 });
|
|
110
|
+
const hydrated = strategy === 'bounded' ? { tools: [], receipt: { hydrated_tools: 0, schema_chars: 0 } } : hydrateToolSchemas(BASE_TOOLS, tools.cards.map(card => card.tool), { maxTools: 8, maxChars: 40_000 });
|
|
111
|
+
let pack = resolveContext({ objective: fx.task, budget, toolSchemaChars: hydrated.receipt.schema_chars, items });
|
|
112
|
+
const selectedRefs = new Set(pack.items.map(item => item.ref));
|
|
113
|
+
|
|
114
|
+
if (strategy === 'compact') {
|
|
115
|
+
for (const ref of fx.required) {
|
|
116
|
+
const selected = pack.items.find(item => item.ref === ref);
|
|
117
|
+
if (!selected?.compacted) continue;
|
|
118
|
+
const source = sources.get(ref);
|
|
119
|
+
const rehydrated = await rehydrateContextRef(ref, async () => source, { kind: source.kind, maxChars: budget.maxFileCharsPerRead });
|
|
120
|
+
const remaining = pack.items.filter(item => item.ref !== ref);
|
|
121
|
+
remaining.push({ ...rehydrated, priority: source.priority });
|
|
122
|
+
pack = resolveContext({ objective: fx.task, budget, toolSchemaChars: hydrated.receipt.schema_chars, items: remaining });
|
|
123
|
+
selectedRefs.add(ref);
|
|
124
|
+
rehydratedRefs.push(ref);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const missingRequired = fx.required.filter(ref => !selectedRefs.has(ref));
|
|
129
|
+
const evidenceTokens = pack.receipt.estimated_input_tokens;
|
|
130
|
+
const activeTokens = fx.syntheticBaseTokens + evidenceTokens;
|
|
131
|
+
const pressure = assessContextUsage(activeTokens, budget);
|
|
132
|
+
const inputCost = calculateModelCost(record, { input_tokens: activeTokens, output_tokens: 0 }, { serviceTier: 'default' });
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
strategy,
|
|
135
|
+
result: missingRequired.length ? 'FAIL' : 'PASS',
|
|
136
|
+
required_evidence: fx.required.length,
|
|
137
|
+
required_found: fx.required.length - missingRequired.length,
|
|
138
|
+
missing_required: Object.freeze(missingRequired),
|
|
139
|
+
sources_considered: pack.receipt.sources_considered,
|
|
140
|
+
sources_selected: pack.receipt.sources_included,
|
|
141
|
+
sources_deferred: pack.receipt.sources_deferred,
|
|
142
|
+
tool_cards: tools.receipt.returned_items,
|
|
143
|
+
tool_card_chars: tools.receipt.chars,
|
|
144
|
+
hydrated_tools: hydrated.receipt.hydrated_tools,
|
|
145
|
+
tool_schema_chars: hydrated.receipt.schema_chars,
|
|
146
|
+
compacted_chars: compactedChars,
|
|
147
|
+
rehydrated_refs: Object.freeze(rehydratedRefs),
|
|
148
|
+
active_context_tokens: activeTokens,
|
|
149
|
+
evidence_tokens: evidenceTokens,
|
|
150
|
+
window_tokens: budget.windowTokens,
|
|
151
|
+
pricing_threshold_tokens: budget.pricingThresholdTokens,
|
|
152
|
+
tokens_until_pricing_threshold: pressure.tokensUntilPricingThreshold,
|
|
153
|
+
pressure: pressure.stage,
|
|
154
|
+
should_compact: pressure.shouldCompact,
|
|
155
|
+
estimated_input_cost_usd: inputCost.total_usd,
|
|
156
|
+
estimate_kind: 'local',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rank(results) {
|
|
161
|
+
return [...results].sort((a, b) => {
|
|
162
|
+
if (a.result !== b.result) return a.result === 'PASS' ? -1 : 1;
|
|
163
|
+
if (a.required_found !== b.required_found) return b.required_found - a.required_found;
|
|
164
|
+
if (a.active_context_tokens !== b.active_context_tokens) return a.active_context_tokens - b.active_context_tokens;
|
|
165
|
+
return a.tool_schema_chars - b.tool_schema_chars;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function listContextEvalFixtures() { return Object.keys(FIXTURES); }
|
|
170
|
+
|
|
171
|
+
export async function evaluateContextFixture(options = {}) {
|
|
172
|
+
const name = options.fixture || 'exact-symbol-callers';
|
|
173
|
+
const fx = fixture(name);
|
|
174
|
+
const record = getModelRecord(options.model || 'gpt-6-astra');
|
|
175
|
+
if (!record) throw new Error(`unknown model: ${options.model}`);
|
|
176
|
+
const strategies = options.strategy && options.strategy !== 'all' ? [options.strategy] : STRATEGIES;
|
|
177
|
+
for (const strategy of strategies) if (!STRATEGIES.includes(strategy)) throw new Error(`unknown context strategy: ${strategy}`);
|
|
178
|
+
const results = [];
|
|
179
|
+
for (const strategy of strategies) results.push(await runStrategy(fx, strategy, record));
|
|
180
|
+
const ranked = rank(results);
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
schema_version: 1,
|
|
183
|
+
fixture: name,
|
|
184
|
+
task: fx.task,
|
|
185
|
+
model: record.provider_model_id,
|
|
186
|
+
live_provider_used: false,
|
|
187
|
+
strategies: Object.freeze(results),
|
|
188
|
+
winner: ranked[0]?.strategy || null,
|
|
189
|
+
scoring: Object.freeze(['correctness', 'required_evidence', 'context_efficiency', 'tool_efficiency']),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { listContextEvalFixtures, evaluateContextFixture } from './context.js';
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import pkg from '../package.json' with { type: 'json' };
|
|
|
4
4
|
|
|
5
5
|
export { AgentSam } from './AgentSam.js';
|
|
6
6
|
export { routeIntent } from './lib/router.js';
|
|
7
|
-
export { searchToolCards, toToolCard } from './tools/index.js';
|
|
7
|
+
export { searchToolCards, toToolCard, hydrateToolSchemas } from './tools/index.js';
|
|
8
8
|
export { getToolCatalog } from './lib/tools.js';
|
|
9
9
|
export { scaffoldProject } from './lib/scaffold.js';
|
|
10
10
|
export {
|
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
normalizeGitRemote,
|
|
20
20
|
resolveGitContext,
|
|
21
21
|
tryResolveGitContext,
|
|
22
|
-
} from '
|
|
22
|
+
} from '../packages/agentsam-repository/src/git-context.js';
|
|
23
23
|
export {
|
|
24
24
|
resolveAgentSamBaseUrl,
|
|
25
25
|
resolveBridgeKey,
|
|
@@ -40,21 +40,87 @@ export {
|
|
|
40
40
|
repositorySnapshot,
|
|
41
41
|
} from './capabilities/index.js';
|
|
42
42
|
export { getPreset, listPresets, resolvePreset, getAddon, listAddons } from './presets/index.js';
|
|
43
|
+
export {
|
|
44
|
+
COMPANY_REPOSITORY_GRAPH_SCHEMA_VERSION,
|
|
45
|
+
REPOSITORY_STATUSES,
|
|
46
|
+
REPOSITORY_CONTRACT_TYPES,
|
|
47
|
+
REPOSITORY_CONTRACT_STATUSES,
|
|
48
|
+
REPOSITORY_DEPENDENCY_TYPES,
|
|
49
|
+
REPOSITORY_DEPENDENCY_CRITICALITIES,
|
|
50
|
+
REPOSITORY_FAILURE_POLICIES,
|
|
51
|
+
createRepositoryIdentity,
|
|
52
|
+
createRepositoryContract,
|
|
53
|
+
createRepositoryDependency,
|
|
54
|
+
} from '../packages/agentsam-repository/src/contracts.js';
|
|
43
55
|
export {
|
|
44
56
|
DEFAULT_CONTEXT_RATIOS,
|
|
45
57
|
DEFAULT_RESULT_POLICY,
|
|
46
58
|
createContextBudget,
|
|
59
|
+
assessContextUsage,
|
|
47
60
|
normalizeResultPolicy,
|
|
48
61
|
resolveContext,
|
|
49
62
|
resolveProjectContext,
|
|
50
63
|
compactConsumedToolResult,
|
|
64
|
+
rehydrateContextRef,
|
|
51
65
|
loadProjectRules,
|
|
66
|
+
compileAgentInstructions,
|
|
67
|
+
AGENT_INSTRUCTION_PRECEDENCE,
|
|
52
68
|
} from './context/index.js';
|
|
53
69
|
export {
|
|
54
70
|
assertRepositoryKnowledgeProvider,
|
|
55
71
|
createRepositoryKnowledgeClient,
|
|
56
72
|
describeRepositoryKnowledgeProvider,
|
|
57
73
|
} from './indexing/index.js';
|
|
74
|
+
export {
|
|
75
|
+
MODEL_CATALOG_SCHEMA,
|
|
76
|
+
MODEL_CATALOG,
|
|
77
|
+
listModelCatalog,
|
|
78
|
+
getModelRecord,
|
|
79
|
+
calculateModelCost,
|
|
80
|
+
} from './models/index.js';
|
|
81
|
+
export {
|
|
82
|
+
AGENT_EVENT_TYPES,
|
|
83
|
+
RUNTIME_RECEIPT_SCHEMA_VERSION,
|
|
84
|
+
createAgentEvent,
|
|
85
|
+
createUsageSnapshot,
|
|
86
|
+
createRunReceipt,
|
|
87
|
+
createUsageReceipt,
|
|
88
|
+
createApprovalReceipt,
|
|
89
|
+
createTerminalJobReceipt,
|
|
90
|
+
} from './telemetry/index.js';
|
|
91
|
+
export {
|
|
92
|
+
createOpenAIResponsesAdapter,
|
|
93
|
+
extractOpenAIOutputText,
|
|
94
|
+
extractOpenAIFunctionCalls,
|
|
95
|
+
} from './providers/index.js';
|
|
96
|
+
export {
|
|
97
|
+
AgentSamDiagnosticError,
|
|
98
|
+
classifyOpenAIError,
|
|
99
|
+
createOpenAIHttpError,
|
|
100
|
+
createProcessDiagnosticError,
|
|
101
|
+
diagnosticFromError,
|
|
102
|
+
redactDiagnosticValue,
|
|
103
|
+
renderDiagnosticError,
|
|
104
|
+
} from './errors/index.js';
|
|
105
|
+
export {
|
|
106
|
+
WRANGLER_NATIVE_COMMANDS,
|
|
107
|
+
WRANGLER_OPERATION_FAMILIES,
|
|
108
|
+
buildWranglerInvocation,
|
|
109
|
+
listWranglerNativeCommands,
|
|
110
|
+
parseWranglerErrorEvidence,
|
|
111
|
+
runWranglerNative,
|
|
112
|
+
summarizeCloudflareCpuProfile,
|
|
113
|
+
summarizeCloudflareCpuProfileFile,
|
|
114
|
+
buildCloudflareCpuAuditPacket,
|
|
115
|
+
runCloudflareCpuAudit,
|
|
116
|
+
} from './cloudflare/index.js';
|
|
117
|
+
export {
|
|
118
|
+
createCapabilityAdapter,
|
|
119
|
+
buildAgentToolSurface,
|
|
120
|
+
capabilityFunctionName,
|
|
121
|
+
runResponsesAgent,
|
|
122
|
+
} from './agent/index.js';
|
|
123
|
+
export { listContextEvalFixtures, evaluateContextFixture } from './eval/index.js';
|
|
58
124
|
|
|
59
125
|
export {
|
|
60
126
|
createIdentityClient,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function createBearerTokenVerifier(token) {
|
|
4
|
+
if (typeof token !== 'string' || token.length < 32 || token.length > 256) {
|
|
5
|
+
throw new Error('Service token must be 32..256 characters.');
|
|
6
|
+
}
|
|
7
|
+
const secret = Buffer.from(token);
|
|
8
|
+
return authorization => {
|
|
9
|
+
const raw = typeof authorization === 'string' ? authorization : '';
|
|
10
|
+
const provided = Buffer.from(raw.replace(/^Bearer /, ''));
|
|
11
|
+
return provided.length === secret.length && timingSafeEqual(provided, secret);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
grpc,
|
|
3
|
+
KnowledgeServiceClient,
|
|
4
|
+
decodeJob,
|
|
5
|
+
decodeJobEvent,
|
|
6
|
+
decodeRepositoryList,
|
|
7
|
+
encodeGetJobRequest,
|
|
8
|
+
encodeSubmitRequest,
|
|
9
|
+
fromGrpcError,
|
|
10
|
+
knowledgePb,
|
|
11
|
+
metadataForToken,
|
|
12
|
+
} from './grpc-codec.js';
|
|
13
|
+
|
|
14
|
+
function normalizeTimeout(value, fallback) {
|
|
15
|
+
const timeout = value === undefined ? fallback : value;
|
|
16
|
+
if (timeout == null) return null;
|
|
17
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('timeoutMs must be a positive number or null.');
|
|
18
|
+
return timeout;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateJobId(id) {
|
|
22
|
+
if (typeof id !== 'string' || !/^[a-f0-9-]{36}$/.test(id)) throw new Error('Invalid job id.');
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Node/native gRPC client. Keep this separate from the fetch-only Worker-safe client. */
|
|
27
|
+
export function createKnowledgeGrpcClient({
|
|
28
|
+
target,
|
|
29
|
+
token,
|
|
30
|
+
timeoutMs = 15000,
|
|
31
|
+
credentials = grpc.credentials.createInsecure(),
|
|
32
|
+
clientOptions,
|
|
33
|
+
} = {}) {
|
|
34
|
+
if (typeof target !== 'string' || !target.trim()) throw new Error('A gRPC target is required.');
|
|
35
|
+
if (!token) throw new Error('A service token is required.');
|
|
36
|
+
const client = new KnowledgeServiceClient(target.trim(), credentials, clientOptions);
|
|
37
|
+
|
|
38
|
+
const unary = (method, request, options = {}) => new Promise((resolve, reject) => {
|
|
39
|
+
const timeout = normalizeTimeout(options.timeoutMs, timeoutMs);
|
|
40
|
+
const callOptions = timeout == null ? {} : { deadline: new Date(Date.now() + timeout) };
|
|
41
|
+
client[method](request, metadataForToken(token), callOptions, (error, response) => {
|
|
42
|
+
if (error) reject(fromGrpcError(error));
|
|
43
|
+
else resolve(response);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function repositories(options = {}) {
|
|
48
|
+
const response = await unary('listRepositories', new knowledgePb.ListRepositoriesRequest(), options);
|
|
49
|
+
return decodeRepositoryList(response);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function submit(body, idempotencyKey, options = {}) {
|
|
53
|
+
const response = await unary('submitJob', encodeSubmitRequest(body, idempotencyKey), options);
|
|
54
|
+
return decodeJob(response);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function job(id, options = {}) {
|
|
58
|
+
const response = await unary('getJob', encodeGetJobRequest(validateJobId(id)), options);
|
|
59
|
+
return decodeJob(response);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function* watch(id, options = {}) {
|
|
63
|
+
validateJobId(id);
|
|
64
|
+
const timeout = normalizeTimeout(options.timeoutMs, null);
|
|
65
|
+
const callOptions = timeout == null ? {} : { deadline: new Date(Date.now() + timeout) };
|
|
66
|
+
const stream = client.watchJob(
|
|
67
|
+
encodeGetJobRequest(id, knowledgePb.WatchJobRequest),
|
|
68
|
+
metadataForToken(token),
|
|
69
|
+
callOptions,
|
|
70
|
+
);
|
|
71
|
+
const queue = [];
|
|
72
|
+
let done = false;
|
|
73
|
+
let failure = null;
|
|
74
|
+
let wake = null;
|
|
75
|
+
const notify = () => { const resolve = wake; wake = null; resolve?.(); };
|
|
76
|
+
const onData = event => { queue.push(decodeJobEvent(event)); notify(); };
|
|
77
|
+
const onEnd = () => { done = true; notify(); };
|
|
78
|
+
const onError = error => {
|
|
79
|
+
if (options.signal?.aborted && error?.code === grpc.status.CANCELLED) {
|
|
80
|
+
done = true;
|
|
81
|
+
} else {
|
|
82
|
+
failure = fromGrpcError(error);
|
|
83
|
+
done = true;
|
|
84
|
+
}
|
|
85
|
+
notify();
|
|
86
|
+
};
|
|
87
|
+
const onAbort = () => stream.cancel();
|
|
88
|
+
stream.on('data', onData);
|
|
89
|
+
stream.once('end', onEnd);
|
|
90
|
+
stream.once('error', onError);
|
|
91
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
92
|
+
if (options.signal?.aborted) onAbort();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
while (!done || queue.length) {
|
|
96
|
+
if (!queue.length) await new Promise(resolve => { wake = resolve; });
|
|
97
|
+
while (queue.length) yield queue.shift();
|
|
98
|
+
if (failure) throw failure;
|
|
99
|
+
}
|
|
100
|
+
if (failure) throw failure;
|
|
101
|
+
} finally {
|
|
102
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
103
|
+
stream.off('data', onData);
|
|
104
|
+
if (!done) stream.cancel();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
repositories,
|
|
110
|
+
submit,
|
|
111
|
+
job,
|
|
112
|
+
watch,
|
|
113
|
+
close() { client.close(); },
|
|
114
|
+
});
|
|
115
|
+
}
|