@mikode13/harness 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/engines/domain/model/llmAgent.d.ts +1 -1
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentFactory.d.ts +15 -46
- package/dist/factory/infrastructure/agentFactory.js +46 -77
- package/dist/factory/infrastructure/agentFactory.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +44 -0
- package/dist/factory/infrastructure/agentLLMFactory.js +98 -0
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -0
- package/dist/factory/infrastructure/types.d.ts +33 -0
- package/dist/factory/infrastructure/types.js +3 -0
- package/dist/factory/infrastructure/types.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +1 -8
- package/dist/llm/domain/tool.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +6 -2
- package/dist/llm/infrastructure/claudeLLMClient.js +26 -5
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +5 -1
- package/dist/llm/infrastructure/openAILLMClient.js +17 -3
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/instructedAgent.d.ts +15 -0
- package/dist/orchestration/domain/model/instructedAgent.js +17 -0
- package/dist/orchestration/domain/model/instructedAgent.js.map +1 -0
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +3 -0
- package/dist/orchestration/domain/model/orchestratorAgent.js +9 -9
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/tools/domain/tool.d.ts +9 -0
- package/dist/tools/domain/tool.js +2 -0
- package/dist/tools/domain/tool.js.map +1 -0
- package/dist/tools/domain/workspace.d.ts +48 -0
- package/dist/tools/domain/workspace.js +2 -0
- package/dist/tools/domain/workspace.js.map +1 -0
- package/dist/tools/infrastructure/boundedWorkspace.d.ts +66 -0
- package/dist/tools/infrastructure/boundedWorkspace.js +160 -0
- package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/createWorkspace.d.ts +14 -0
- package/dist/tools/infrastructure/createWorkspace.js +54 -0
- package/dist/tools/infrastructure/createWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/defineTool.d.ts +15 -0
- package/dist/tools/infrastructure/defineTool.js +87 -0
- package/dist/tools/infrastructure/defineTool.js.map +1 -0
- package/dist/tools/infrastructure/gitWorkspace.d.ts +13 -0
- package/dist/tools/infrastructure/gitWorkspace.js +61 -0
- package/dist/tools/infrastructure/gitWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +20 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.js +85 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/runProcess.d.ts +23 -0
- package/dist/tools/infrastructure/runProcess.js +91 -0
- package/dist/tools/infrastructure/runProcess.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +4 -0
- package/dist/tools/infrastructure/workspaceTools.js +110 -0
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -0
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestratorAgent.js","sourceRoot":"","sources":["../../../../src/orchestration/domain/model/orchestratorAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentResponse, Callback } from '#src/agent/domain/agent';\nimport { addTokens, type Tokens } from '#src/shared/domain/tokens';\nimport { RecoverableError, UnrecoverableError, withSpentTokens } from '#src/agent/domain/errors';\nimport type { ReviewerDecision } from './reviewerDecision.ts';\nimport type { Validator } from '../interface/validator.ts';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';\n\nconst getPlannerPrompt = (userPrompt: string, previousFailureReason?: string) => {\n\tconst feedback = previousFailureReason\n\t\t? `\\n\\nFeedback from the previous attempt:\\n---\\n${previousFailureReason}\\n---`\n\t\t: '';\n\n\treturn `You are the planner agent. Read the relevant repository code and create a concise, engineering-grade plan for the executor. Cover the requested behaviour, structural issues, affected files or symbols, API contracts and boundaries, important failure paths, and meaningful tests. Keep the plan focused on the current request and do not require an architecture redesign yet.\n\nOriginal user request:\n---\n${userPrompt}\n---${feedback}`;\n};\n\nconst getExecutorPrompt = (userPrompt: string, plannerPrompt: string) =>\n\t`You are the executor agent. Read the relevant repository code and implement the original user request according to the planner's current plan. Preserve contracts and boundaries, handle important failure paths, keep the change focused, and add or update meaningful tests. Inspect and change the code; do not merely describe what should be done.\n\nOriginal user request:\n---\n${userPrompt}\n---\n\nCurrent implementation plan:\n---\n${plannerPrompt}\n---`;\n\nconst getReviewerPrompt = (\n\tuserPrompt: string,\n\tplannerPrompt: string,\n\texecutorResult: string,\n\tparseFailureReason?: string,\n) => {\n\tconst retryNotice = parseFailureReason\n\t\t? `\\n\\nYour previous response could not be used: ${parseFailureReason} Respond with JSON only, matching the schema exactly, with no surrounding text and no markdown code fences.`\n\t\t: '';\n\n\treturn `You are the reviewer agent. Independently inspect the repository, the current diff, and relevant surrounding code; do not rely on the executor's narrative. Evaluate the original request first; plan compliance is secondary and provides supporting context. Check correctness and logic errors, important failure paths, unused or artificial abstractions, races or shared state, API contract breaks, boundary violations, regressions, scope, and test quality. If it is correct, respond with JSON only: {\"decision\":\"approved\"}. If it is not correct, respond with JSON only: {\"decision\":\"rejected\",\"feedback\":\"list concrete, prioritized findings and the required direction for each\"}. The feedback must contain actionable engineering findings, not a general summary.\n\nOriginal user request:\n---\n${userPrompt}\n---\n\nPlanner's current plan:\n---\n${plannerPrompt}\n---\n\nExecutor response (context only; verify it independently):\n---\n${executorResult}\n---${retryNotice}`;\n};\n\ninterface RunTotals {\n\ttokens: Tokens | undefined;\n\t/** A role answered without usage, so the run's total cannot be known. */\n\tunreported: boolean;\n}\n\n// Every role's response counts, even an empty one that starts another round: it was billed.\nfunction addToTotals(totals: RunTotals, response: AgentResponse): void {\n\tif (!response.tokens) totals.unreported = true;\n\ttotals.tokens = addTokens(totals.tokens, response.tokens);\n}\n\n/** Unknown rather than partial: a sum missing a billed call must not look complete. */\nfunction runTokens(totals: RunTotals): Tokens | undefined {\n\treturn totals.unreported ? undefined : totals.tokens;\n}\n\nfunction stripCodeFence(text: string): string {\n\tconst match = /^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/.exec(text.trim());\n\treturn match?.[1] ?? text;\n}\n\nfunction parseReviewerDecision(\n\tresponse: AgentResponse,\n\treviewerDecisionValidator: Validator<ReviewerDecision>,\n): ReviewerDecision {\n\tif (!response.response) {\n\t\tthrow new RecoverableError(\"There's no decision from the reviewer, something went wrong\", {\n\t\t\tcause: 'Reviewer decision is missing.',\n\t\t});\n\t}\n\n\tlet parsedResponse: unknown;\n\ttry {\n\t\tparsedResponse = JSON.parse(stripCodeFence(response.response));\n\t} catch {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response must be valid JSON.',\n\t\t});\n\t}\n\n\tconst decision = reviewerDecisionValidator.validate(parsedResponse);\n\tif (!decision) {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response did not match the decision schema.',\n\t\t});\n\t}\n\n\treturn decision;\n}\n\nexport class OrchestratorAgent implements Agent {\n\tprivate plannerAgent: Agent;\n\tprivate executorAgent: Agent;\n\tprivate reviewerAgent: Agent;\n\tprivate reviewerDecisionValidator: Validator<ReviewerDecision>;\n\tprivate maxAttempts: number;\n\tprivate logger: ILogger;\n\n\tconstructor({\n\t\tplannerAgent,\n\t\texecutorAgent,\n\t\treviewerAgent,\n\t\treviewerDecisionValidator,\n\t\tmaxAttempts = 3,\n\t\tlogger,\n\t}: {\n\t\tplannerAgent: Agent;\n\t\texecutorAgent: Agent;\n\t\treviewerAgent: Agent;\n\t\treviewerDecisionValidator: Validator<ReviewerDecision>;\n\t\tmaxAttempts?: number;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n\t\t\tthrow new RangeError('maxAttempts must be a positive integer');\n\t\t}\n\n\t\tthis.plannerAgent = plannerAgent;\n\t\tthis.executorAgent = executorAgent;\n\t\tthis.reviewerAgent = reviewerAgent;\n\t\tthis.reviewerDecisionValidator = reviewerDecisionValidator;\n\t\tthis.maxAttempts = maxAttempts;\n\t\tthis.logger = logger;\n\t}\n\n\tasync run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\t// Per invocation, not per instance: the CLI keeps one orchestrator for a whole session.\n\t\tconst totals: RunTotals = { tokens: undefined, unreported: false };\n\n\t\ttry {\n\t\t\tawait this.runRounds(prompt, signal, callback, totals);\n\t\t} catch (error) {\n\t\t\t// A failing role carries its own tokens; the roles before it are in the totals.\n\t\t\tthrow withSpentTokens(error, totals.tokens, totals.unreported);\n\t\t}\n\n\t\treturn {\n\t\t\tresponse: 'All job has finished',\n\t\t\ttokens: runTokens(totals),\n\t\t\t// The whole run's wall clock, so it matches what the consumer waited.\n\t\t\tduration: (Date.now() - start) / 1000,\n\t\t};\n\t}\n\n\tprivate async runRounds(\n\t\tprompt: string,\n\t\tsignal: AbortSignal,\n\t\tcallback: Callback,\n\t\ttotals: RunTotals,\n\t): Promise<void> {\n\t\tlet lastFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst isLastAttempt = attempt === this.maxAttempts;\n\n\t\t\tconst plannerResponse = await this.plannerAgent.run(\n\t\t\t\tgetPlannerPrompt(prompt, lastFailureReason),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\t\t\taddToTotals(totals, plannerResponse);\n\n\t\t\tif (!plannerResponse.response) {\n\t\t\t\tlastFailureReason = 'The planner produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the planner produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst executorResponse = await this.executorAgent.run(\n\t\t\t\tgetExecutorPrompt(prompt, plannerResponse.response),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\t\t\taddToTotals(totals, executorResponse);\n\n\t\t\tif (!executorResponse.response) {\n\t\t\t\tlastFailureReason = 'The executor produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the executor produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst reviewerDecision = await this.getReviewerDecision(\n\t\t\t\tprompt,\n\t\t\t\tplannerResponse.response,\n\t\t\t\texecutorResponse.response,\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t\ttotals,\n\t\t\t);\n\n\t\t\tif (reviewerDecision.decision === 'approved') return;\n\n\t\t\tlastFailureReason = reviewerDecision.feedback;\n\t\t\tif (isLastAttempt) {\n\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t}\n\n\t\t\tthis.warn(\n\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the reviewer rejected the round; starting another with its feedback`,\n\t\t\t\tlastFailureReason,\n\t\t\t);\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: lastFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\t// Retries only the reviewer call on a malformed decision, instead of redoing\n\t// planning/execution — a bad or unparsable reviewer response is not evidence\n\t// the plan or the implementation were wrong.\n\tprivate async getReviewerDecision(\n\t\tuserPrompt: string,\n\t\tplannerPrompt: string,\n\t\texecutorResult: string,\n\t\tsignal: AbortSignal,\n\t\tcallback: Callback,\n\t\ttotals: RunTotals,\n\t): Promise<ReviewerDecision> {\n\t\tlet parseFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst reviewerResponse = await this.reviewerAgent.run(\n\t\t\t\tgetReviewerPrompt(userPrompt, plannerPrompt, executorResult, parseFailureReason),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\n\t\t\taddToTotals(totals, reviewerResponse);\n\n\t\t\ttry {\n\t\t\t\treturn parseReviewerDecision(reviewerResponse, this.reviewerDecisionValidator);\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof RecoverableError)) throw error;\n\n\t\t\t\tparseFailureReason = error.cause;\n\t\t\t\tif (attempt === this.maxAttempts) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: parseFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Reviewer decision ${String(attempt)}/${String(this.maxAttempts)} was unusable; asking the reviewer again`,\n\t\t\t\t\tparseFailureReason,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: parseFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\tprivate warn(...args: unknown[]): void {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tthis.logger.warn(...args);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Orchestrator logger failed while reporting a retry',\n\t\t);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"orchestratorAgent.js","sourceRoot":"","sources":["../../../../src/orchestration/domain/model/orchestratorAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentResponse, Callback } from '#src/agent/domain/agent';\nimport { addTokens, type Tokens } from '#src/shared/domain/tokens';\nimport { RecoverableError, UnrecoverableError, withSpentTokens } from '#src/agent/domain/errors';\nimport type { ReviewerDecision } from './reviewerDecision.ts';\nimport type { Validator } from '../interface/validator.ts';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';\n\n// Each role's standing instructions, kept apart from the data of a round so an agent with a\n// system prompt can hold them there instead of receiving them again in every prompt. An agent\n// without one gets them at the head of each prompt; see `InstructedAgent`.\nexport const plannerInstructions =\n\t'You are the planner agent. Read the relevant repository code and create a concise, engineering-grade plan for the executor. Cover the requested behaviour, structural issues, affected files or symbols, API contracts and boundaries, important failure paths, and meaningful tests. Keep the plan focused on the current request and do not require an architecture redesign yet.';\n\nexport const executorInstructions =\n\t\"You are the executor agent. Read the relevant repository code and implement the original user request according to the planner's current plan. Preserve contracts and boundaries, handle important failure paths, keep the change focused, and add or update meaningful tests. Inspect and change the code; do not merely describe what should be done.\";\n\nexport const reviewerInstructions =\n\t'You are the reviewer agent. Independently inspect the repository, the current diff, and relevant surrounding code; do not rely on the executor\\'s narrative. Evaluate the original request first; plan compliance is secondary and provides supporting context. Check correctness and logic errors, important failure paths, unused or artificial abstractions, races or shared state, API contract breaks, boundary violations, regressions, scope, and test quality. If it is correct, respond with JSON only: {\"decision\":\"approved\"}. If it is not correct, respond with JSON only: {\"decision\":\"rejected\",\"feedback\":\"list concrete, prioritized findings and the required direction for each\"}. The feedback must contain actionable engineering findings, not a general summary.';\n\nconst getPlannerPrompt = (userPrompt: string, previousFailureReason?: string) => {\n\tconst feedback = previousFailureReason\n\t\t? `\\n\\nFeedback from the previous attempt:\\n---\\n${previousFailureReason}\\n---`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---${feedback}`;\n};\n\nconst getExecutorPrompt = (userPrompt: string, plannerPrompt: string) =>\n\t`Original user request:\n---\n${userPrompt}\n---\n\nCurrent implementation plan:\n---\n${plannerPrompt}\n---`;\n\nconst getReviewerPrompt = (\n\tuserPrompt: string,\n\tplannerPrompt: string,\n\texecutorResult: string,\n\tparseFailureReason?: string,\n) => {\n\tconst retryNotice = parseFailureReason\n\t\t? `\\n\\nYour previous response could not be used: ${parseFailureReason} Respond with JSON only, matching the schema exactly, with no surrounding text and no markdown code fences.`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---\n\nPlanner's current plan:\n---\n${plannerPrompt}\n---\n\nExecutor response (context only; verify it independently):\n---\n${executorResult}\n---${retryNotice}`;\n};\n\ninterface RunTotals {\n\ttokens: Tokens | undefined;\n\t/** A role answered without usage, so the run's total cannot be known. */\n\tunreported: boolean;\n}\n\n// Every role's response counts, even an empty one that starts another round: it was billed.\nfunction addToTotals(totals: RunTotals, response: AgentResponse): void {\n\tif (!response.tokens) totals.unreported = true;\n\ttotals.tokens = addTokens(totals.tokens, response.tokens);\n}\n\n/** Unknown rather than partial: a sum missing a billed call must not look complete. */\nfunction runTokens(totals: RunTotals): Tokens | undefined {\n\treturn totals.unreported ? undefined : totals.tokens;\n}\n\nfunction stripCodeFence(text: string): string {\n\tconst match = /^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/.exec(text.trim());\n\treturn match?.[1] ?? text;\n}\n\nfunction parseReviewerDecision(\n\tresponse: AgentResponse,\n\treviewerDecisionValidator: Validator<ReviewerDecision>,\n): ReviewerDecision {\n\tif (!response.response) {\n\t\tthrow new RecoverableError(\"There's no decision from the reviewer, something went wrong\", {\n\t\t\tcause: 'Reviewer decision is missing.',\n\t\t});\n\t}\n\n\tlet parsedResponse: unknown;\n\ttry {\n\t\tparsedResponse = JSON.parse(stripCodeFence(response.response));\n\t} catch {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response must be valid JSON.',\n\t\t});\n\t}\n\n\tconst decision = reviewerDecisionValidator.validate(parsedResponse);\n\tif (!decision) {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response did not match the decision schema.',\n\t\t});\n\t}\n\n\treturn decision;\n}\n\nexport class OrchestratorAgent implements Agent {\n\tprivate plannerAgent: Agent;\n\tprivate executorAgent: Agent;\n\tprivate reviewerAgent: Agent;\n\tprivate reviewerDecisionValidator: Validator<ReviewerDecision>;\n\tprivate maxAttempts: number;\n\tprivate logger: ILogger;\n\n\tconstructor({\n\t\tplannerAgent,\n\t\texecutorAgent,\n\t\treviewerAgent,\n\t\treviewerDecisionValidator,\n\t\tmaxAttempts = 3,\n\t\tlogger,\n\t}: {\n\t\tplannerAgent: Agent;\n\t\texecutorAgent: Agent;\n\t\treviewerAgent: Agent;\n\t\treviewerDecisionValidator: Validator<ReviewerDecision>;\n\t\tmaxAttempts?: number;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n\t\t\tthrow new RangeError('maxAttempts must be a positive integer');\n\t\t}\n\n\t\tthis.plannerAgent = plannerAgent;\n\t\tthis.executorAgent = executorAgent;\n\t\tthis.reviewerAgent = reviewerAgent;\n\t\tthis.reviewerDecisionValidator = reviewerDecisionValidator;\n\t\tthis.maxAttempts = maxAttempts;\n\t\tthis.logger = logger;\n\t}\n\n\tasync run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\t// Per invocation, not per instance: the CLI keeps one orchestrator for a whole session.\n\t\tconst totals: RunTotals = { tokens: undefined, unreported: false };\n\n\t\ttry {\n\t\t\tawait this.runRounds(prompt, signal, callback, totals);\n\t\t} catch (error) {\n\t\t\t// A failing role carries its own tokens; the roles before it are in the totals.\n\t\t\tthrow withSpentTokens(error, totals.tokens, totals.unreported);\n\t\t}\n\n\t\treturn {\n\t\t\tresponse: 'All job has finished',\n\t\t\ttokens: runTokens(totals),\n\t\t\t// The whole run's wall clock, so it matches what the consumer waited.\n\t\t\tduration: (Date.now() - start) / 1000,\n\t\t};\n\t}\n\n\tprivate async runRounds(\n\t\tprompt: string,\n\t\tsignal: AbortSignal,\n\t\tcallback: Callback,\n\t\ttotals: RunTotals,\n\t): Promise<void> {\n\t\tlet lastFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst isLastAttempt = attempt === this.maxAttempts;\n\n\t\t\tconst plannerResponse = await this.plannerAgent.run(\n\t\t\t\tgetPlannerPrompt(prompt, lastFailureReason),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\t\t\taddToTotals(totals, plannerResponse);\n\n\t\t\tif (!plannerResponse.response) {\n\t\t\t\tlastFailureReason = 'The planner produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the planner produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst executorResponse = await this.executorAgent.run(\n\t\t\t\tgetExecutorPrompt(prompt, plannerResponse.response),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\t\t\taddToTotals(totals, executorResponse);\n\n\t\t\tif (!executorResponse.response) {\n\t\t\t\tlastFailureReason = 'The executor produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the executor produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst reviewerDecision = await this.getReviewerDecision(\n\t\t\t\tprompt,\n\t\t\t\tplannerResponse.response,\n\t\t\t\texecutorResponse.response,\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t\ttotals,\n\t\t\t);\n\n\t\t\tif (reviewerDecision.decision === 'approved') return;\n\n\t\t\tlastFailureReason = reviewerDecision.feedback;\n\t\t\tif (isLastAttempt) {\n\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t}\n\n\t\t\tthis.warn(\n\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the reviewer rejected the round; starting another with its feedback`,\n\t\t\t\tlastFailureReason,\n\t\t\t);\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: lastFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\t// Retries only the reviewer call on a malformed decision, instead of redoing\n\t// planning/execution — a bad or unparsable reviewer response is not evidence\n\t// the plan or the implementation were wrong.\n\tprivate async getReviewerDecision(\n\t\tuserPrompt: string,\n\t\tplannerPrompt: string,\n\t\texecutorResult: string,\n\t\tsignal: AbortSignal,\n\t\tcallback: Callback,\n\t\ttotals: RunTotals,\n\t): Promise<ReviewerDecision> {\n\t\tlet parseFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst reviewerResponse = await this.reviewerAgent.run(\n\t\t\t\tgetReviewerPrompt(userPrompt, plannerPrompt, executorResult, parseFailureReason),\n\t\t\t\tsignal,\n\t\t\t\tcallback,\n\t\t\t);\n\n\t\t\taddToTotals(totals, reviewerResponse);\n\n\t\t\ttry {\n\t\t\t\treturn parseReviewerDecision(reviewerResponse, this.reviewerDecisionValidator);\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof RecoverableError)) throw error;\n\n\t\t\t\tparseFailureReason = error.cause;\n\t\t\t\tif (attempt === this.maxAttempts) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: parseFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Reviewer decision ${String(attempt)}/${String(this.maxAttempts)} was unusable; asking the reviewer again`,\n\t\t\t\t\tparseFailureReason,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: parseFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\tprivate warn(...args: unknown[]): void {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tthis.logger.warn(...args);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Orchestrator logger failed while reporting a retry',\n\t\t);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolDefinition } from '#src/llm/domain/tool';
|
|
2
|
+
export interface Tool extends ToolDefinition {
|
|
3
|
+
/**
|
|
4
|
+
* Runs the tool on the input the model sent. The input is unknown because the model can
|
|
5
|
+
* send anything: validating it is the tool's job, and a rejection returns to the model as
|
|
6
|
+
* an error result.
|
|
7
|
+
*/
|
|
8
|
+
execute(input: unknown, signal: AbortSignal): Promise<string>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../../../src/tools/domain/tool.ts"],"names":[],"mappings":"","sourcesContent":["import type { ToolDefinition } from '#src/llm/domain/tool';\n\nexport interface Tool extends ToolDefinition {\n\t/**\n\t * Runs the tool on the input the model sent. The input is unknown because the model can\n\t * send anything: validating it is the tool's job, and a rejection returns to the model as\n\t * an error result.\n\t */\n\texecute(input: unknown, signal: AbortSignal): Promise<string>;\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface TextMatch {
|
|
2
|
+
path: string;
|
|
3
|
+
line: number;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The folder an agent works on, read-only. Every path is relative to its root, written with
|
|
8
|
+
* `/`, and results come sorted. A file `.gitignore` ignores, or a path outside the root, does
|
|
9
|
+
* not exist here: it is never listed, searched or read, whatever scope a query asks for.
|
|
10
|
+
*
|
|
11
|
+
* A binary file is listed, but never searched or read. Every operation stops after a time
|
|
12
|
+
* limit and fails with an error that asks for a narrower query.
|
|
13
|
+
*
|
|
14
|
+
* `total` counts everything the query matched and `truncated` says whether `limit` cut it.
|
|
15
|
+
*/
|
|
16
|
+
export interface Workspace {
|
|
17
|
+
listFiles(query: {
|
|
18
|
+
path?: string;
|
|
19
|
+
glob?: string;
|
|
20
|
+
limit: number;
|
|
21
|
+
}, signal: AbortSignal): Promise<{
|
|
22
|
+
files: string[];
|
|
23
|
+
total: number;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
/** `pattern` is a regular expression. A match's `text` is at most 1,000 characters. */
|
|
27
|
+
searchText(query: {
|
|
28
|
+
pattern: string;
|
|
29
|
+
ignoreCase: boolean;
|
|
30
|
+
path?: string;
|
|
31
|
+
glob?: string;
|
|
32
|
+
limit: number;
|
|
33
|
+
}, signal: AbortSignal): Promise<{
|
|
34
|
+
matches: TextMatch[];
|
|
35
|
+
total: number;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
/** `fromLine` starts at 1. */
|
|
39
|
+
readFile(query: {
|
|
40
|
+
path: string;
|
|
41
|
+
fromLine: number;
|
|
42
|
+
lineCount: number;
|
|
43
|
+
}, signal: AbortSignal): Promise<{
|
|
44
|
+
lines: string[];
|
|
45
|
+
totalLines: number;
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/tools/domain/workspace.ts"],"names":[],"mappings":"","sourcesContent":["export interface TextMatch {\n\tpath: string;\n\tline: number;\n\ttext: string;\n}\n\n/**\n * The folder an agent works on, read-only. Every path is relative to its root, written with\n * `/`, and results come sorted. A file `.gitignore` ignores, or a path outside the root, does\n * not exist here: it is never listed, searched or read, whatever scope a query asks for.\n *\n * A binary file is listed, but never searched or read. Every operation stops after a time\n * limit and fails with an error that asks for a narrower query.\n *\n * `total` counts everything the query matched and `truncated` says whether `limit` cut it.\n */\nexport interface Workspace {\n\tlistFiles(\n\t\tquery: { path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ files: string[]; total: number; truncated: boolean }>;\n\n\t/** `pattern` is a regular expression. A match's `text` is at most 1,000 characters. */\n\tsearchText(\n\t\tquery: { pattern: string; ignoreCase: boolean; path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ matches: TextMatch[]; total: number; truncated: boolean }>;\n\n\t/** `fromLine` starts at 1. */\n\treadFile(\n\t\tquery: { path: string; fromLine: number; lineCount: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ lines: string[]; totalLines: number; truncated: boolean }>;\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { TextMatch, Workspace } from '../domain/workspace.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Everything both implementations share: the boundary and the shape of every answer. A
|
|
4
|
+
* subclass only says which files are visible and where a pattern matches, across the whole
|
|
5
|
+
* root; the scope a model asks for is applied here, to those results, and never handed to the
|
|
6
|
+
* program underneath. ripgrep stops honouring `.gitignore` for a path it is given explicitly,
|
|
7
|
+
* so passing the model's scope down is how a secret would leak. See "`.gitignore` is the
|
|
8
|
+
* boundary of what an agent reads" in decisions.md.
|
|
9
|
+
*
|
|
10
|
+
* Every operation also stops after `timeoutMs`. A timeout is a failure the model reads and
|
|
11
|
+
* narrows its query from, not a cancellation: only the run's own signal cancels the run.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class BoundedWorkspace implements Workspace {
|
|
14
|
+
protected readonly root: string;
|
|
15
|
+
private readonly timeoutMs;
|
|
16
|
+
constructor({ root, timeoutMs }: {
|
|
17
|
+
root: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
});
|
|
20
|
+
/** Every file the program does not ignore, relative to the root, in any order. */
|
|
21
|
+
protected abstract listCandidates(signal: AbortSignal): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Hands `keep` every line matching `pattern` in a file the program does not ignore, in any
|
|
24
|
+
* order, as the program prints it. Nothing is held here: `keep` decides what to store.
|
|
25
|
+
*/
|
|
26
|
+
protected abstract findMatches(pattern: string, ignoreCase: boolean, signal: AbortSignal, keep: (match: TextMatch) => void): Promise<void>;
|
|
27
|
+
listFiles(query: {
|
|
28
|
+
path?: string;
|
|
29
|
+
glob?: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
}, signal: AbortSignal): Promise<{
|
|
32
|
+
files: string[];
|
|
33
|
+
total: number;
|
|
34
|
+
truncated: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
searchText(query: {
|
|
37
|
+
pattern: string;
|
|
38
|
+
ignoreCase: boolean;
|
|
39
|
+
path?: string;
|
|
40
|
+
glob?: string;
|
|
41
|
+
limit: number;
|
|
42
|
+
}, signal: AbortSignal): Promise<{
|
|
43
|
+
matches: TextMatch[];
|
|
44
|
+
total: number;
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
readFile(query: {
|
|
48
|
+
path: string;
|
|
49
|
+
fromLine: number;
|
|
50
|
+
lineCount: number;
|
|
51
|
+
}, signal: AbortSignal): Promise<{
|
|
52
|
+
lines: string[];
|
|
53
|
+
totalLines: number;
|
|
54
|
+
truncated: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Runs an operation under the run's signal and this workspace's time limit together. The
|
|
58
|
+
* run's cancellation escapes unchanged; the time limit becomes an error the model can act on.
|
|
59
|
+
*/
|
|
60
|
+
private withTimeout;
|
|
61
|
+
/** Regular files only: a symlink could point out of the root, and a deleted file is gone. */
|
|
62
|
+
private regularFiles;
|
|
63
|
+
private inScope;
|
|
64
|
+
/** A path relative to the root with `/`, '' for the root itself, or undefined when it leaves it. */
|
|
65
|
+
private toRelative;
|
|
66
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
// A NUL byte this early means a binary file, as git and ripgrep both judge it.
|
|
6
|
+
const binaryProbeBytes = 8000;
|
|
7
|
+
// What a search holds while it sorts. A minified file can put a whole bundle on one line, and
|
|
8
|
+
// a careless pattern can match every line of a repository.
|
|
9
|
+
const maxStoredMatches = 50_000;
|
|
10
|
+
const maxStoredLineLength = 1_000;
|
|
11
|
+
// Far beyond what ripgrep needs on a large repository, so only a runaway search reaches it,
|
|
12
|
+
// such as a pattern that backtracks without end in git's Perl engine.
|
|
13
|
+
const defaultTimeoutMs = 30_000;
|
|
14
|
+
function compareText(a, b) {
|
|
15
|
+
// Plain code-unit order: `localeCompare` would sort differently on every machine.
|
|
16
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Everything both implementations share: the boundary and the shape of every answer. A
|
|
20
|
+
* subclass only says which files are visible and where a pattern matches, across the whole
|
|
21
|
+
* root; the scope a model asks for is applied here, to those results, and never handed to the
|
|
22
|
+
* program underneath. ripgrep stops honouring `.gitignore` for a path it is given explicitly,
|
|
23
|
+
* so passing the model's scope down is how a secret would leak. See "`.gitignore` is the
|
|
24
|
+
* boundary of what an agent reads" in decisions.md.
|
|
25
|
+
*
|
|
26
|
+
* Every operation also stops after `timeoutMs`. A timeout is a failure the model reads and
|
|
27
|
+
* narrows its query from, not a cancellation: only the run's own signal cancels the run.
|
|
28
|
+
*/
|
|
29
|
+
export class BoundedWorkspace {
|
|
30
|
+
root;
|
|
31
|
+
timeoutMs;
|
|
32
|
+
constructor({ root, timeoutMs = defaultTimeoutMs }) {
|
|
33
|
+
this.root = root;
|
|
34
|
+
this.timeoutMs = timeoutMs;
|
|
35
|
+
}
|
|
36
|
+
listFiles(query, signal) {
|
|
37
|
+
return this.withTimeout(signal, async (bounded) => {
|
|
38
|
+
const inScope = [...new Set(await this.listCandidates(bounded))].filter(path => this.inScope(path, query));
|
|
39
|
+
const files = (await this.regularFiles(inScope)).sort(compareText);
|
|
40
|
+
return {
|
|
41
|
+
files: files.slice(0, query.limit),
|
|
42
|
+
total: files.length,
|
|
43
|
+
truncated: files.length > query.limit,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
searchText(query, signal) {
|
|
48
|
+
return this.withTimeout(signal, async (bounded) => {
|
|
49
|
+
const listed = new Set(await this.listCandidates(bounded));
|
|
50
|
+
// The scope applies as the output arrives, so narrowing a search is what makes it
|
|
51
|
+
// lighter: matches outside it are never stored, however many the program prints.
|
|
52
|
+
const kept = [];
|
|
53
|
+
await this.findMatches(query.pattern, query.ignoreCase, bounded, match => {
|
|
54
|
+
if (!listed.has(match.path) || !this.inScope(match.path, query)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (kept.length === maxStoredMatches) {
|
|
58
|
+
throw new Error(`More than ${String(maxStoredMatches)} lines match; narrow the pattern, path or glob`);
|
|
59
|
+
}
|
|
60
|
+
kept.push({ ...match, text: match.text.slice(0, maxStoredLineLength) });
|
|
61
|
+
});
|
|
62
|
+
// Only the files that matched are checked on disk, not the whole repository.
|
|
63
|
+
const regular = new Set(await this.regularFiles([...new Set(kept.map(match => match.path))]));
|
|
64
|
+
const matches = kept
|
|
65
|
+
.filter(match => regular.has(match.path))
|
|
66
|
+
.sort((a, b) => compareText(a.path, b.path) || a.line - b.line);
|
|
67
|
+
return {
|
|
68
|
+
matches: matches.slice(0, query.limit),
|
|
69
|
+
total: matches.length,
|
|
70
|
+
truncated: matches.length > query.limit,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
readFile(query, signal) {
|
|
75
|
+
return this.withTimeout(signal, async (bounded) => {
|
|
76
|
+
const path = this.toRelative(query.path);
|
|
77
|
+
const visible = path !== undefined &&
|
|
78
|
+
(await this.listCandidates(bounded)).includes(path) &&
|
|
79
|
+
(await this.regularFiles([path])).length === 1;
|
|
80
|
+
// One answer for a missing file and an ignored one, or the model would learn the secret exists.
|
|
81
|
+
if (!visible) {
|
|
82
|
+
throw new Error(`No such file: ${query.path}`);
|
|
83
|
+
}
|
|
84
|
+
const absolute = join(this.root, path);
|
|
85
|
+
if (await isBinary(absolute)) {
|
|
86
|
+
throw new Error(`Not a text file: ${query.path}`);
|
|
87
|
+
}
|
|
88
|
+
// Streamed, so a large file costs its length in lines, not in memory.
|
|
89
|
+
const reader = createInterface({
|
|
90
|
+
input: createReadStream(absolute, { encoding: 'utf8', signal: bounded }),
|
|
91
|
+
crlfDelay: Infinity,
|
|
92
|
+
});
|
|
93
|
+
const last = query.fromLine + query.lineCount - 1;
|
|
94
|
+
const lines = [];
|
|
95
|
+
let totalLines = 0;
|
|
96
|
+
for await (const line of reader) {
|
|
97
|
+
totalLines++;
|
|
98
|
+
if (totalLines >= query.fromLine && totalLines <= last) {
|
|
99
|
+
lines.push(line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { lines, totalLines, truncated: totalLines > last };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs an operation under the run's signal and this workspace's time limit together. The
|
|
107
|
+
* run's cancellation escapes unchanged; the time limit becomes an error the model can act on.
|
|
108
|
+
*/
|
|
109
|
+
async withTimeout(signal, operation) {
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
112
|
+
try {
|
|
113
|
+
return await operation(AbortSignal.any([signal, timeout]));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
signal.throwIfAborted();
|
|
117
|
+
if (timeout.aborted) {
|
|
118
|
+
throw new Error(`The operation took longer than ${String(this.timeoutMs / 1000)} s; narrow the pattern, path or glob`, { cause: error });
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Regular files only: a symlink could point out of the root, and a deleted file is gone. */
|
|
124
|
+
async regularFiles(paths) {
|
|
125
|
+
const regular = await Promise.all(paths.map(path => lstat(join(this.root, path)).then(stats => stats.isFile(), () => false)));
|
|
126
|
+
return paths.filter((_, index) => regular[index]);
|
|
127
|
+
}
|
|
128
|
+
inScope(path, { path: scope, glob }) {
|
|
129
|
+
if (scope !== undefined) {
|
|
130
|
+
const folder = this.toRelative(scope);
|
|
131
|
+
if (folder === undefined) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (folder !== '' && path !== folder && !path.startsWith(`${folder}/`)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return glob === undefined || matchesGlob(path, glob);
|
|
139
|
+
}
|
|
140
|
+
/** A path relative to the root with `/`, '' for the root itself, or undefined when it leaves it. */
|
|
141
|
+
toRelative(path) {
|
|
142
|
+
const relativePath = relative(this.root, resolve(this.root, path));
|
|
143
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return relativePath.split(sep).join('/');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function isBinary(path) {
|
|
150
|
+
const file = await open(path);
|
|
151
|
+
try {
|
|
152
|
+
const probe = Buffer.alloc(binaryProbeBytes);
|
|
153
|
+
const { bytesRead } = await file.read(probe, 0, probe.length, 0);
|
|
154
|
+
return probe.subarray(0, bytesRead).includes(0);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await file.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=boundedWorkspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundedWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/boundedWorkspace.ts"],"names":[],"mappings":"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","sourcesContent":["import { createReadStream } from 'node:fs';\nimport { lstat, open } from 'node:fs/promises';\nimport { isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport type { TextMatch, Workspace } from '../domain/workspace.ts';\n\n// A NUL byte this early means a binary file, as git and ripgrep both judge it.\nconst binaryProbeBytes = 8000;\n\n// What a search holds while it sorts. A minified file can put a whole bundle on one line, and\n// a careless pattern can match every line of a repository.\nconst maxStoredMatches = 50_000;\nconst maxStoredLineLength = 1_000;\n\n// Far beyond what ripgrep needs on a large repository, so only a runaway search reaches it,\n// such as a pattern that backtracks without end in git's Perl engine.\nconst defaultTimeoutMs = 30_000;\n\nfunction compareText(a: string, b: string): number {\n\t// Plain code-unit order: `localeCompare` would sort differently on every machine.\n\treturn a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Everything both implementations share: the boundary and the shape of every answer. A\n * subclass only says which files are visible and where a pattern matches, across the whole\n * root; the scope a model asks for is applied here, to those results, and never handed to the\n * program underneath. ripgrep stops honouring `.gitignore` for a path it is given explicitly,\n * so passing the model's scope down is how a secret would leak. See \"`.gitignore` is the\n * boundary of what an agent reads\" in decisions.md.\n *\n * Every operation also stops after `timeoutMs`. A timeout is a failure the model reads and\n * narrows its query from, not a cancellation: only the run's own signal cancels the run.\n */\nexport abstract class BoundedWorkspace implements Workspace {\n\tprotected readonly root: string;\n\tprivate readonly timeoutMs: number;\n\n\tconstructor({ root, timeoutMs = defaultTimeoutMs }: { root: string; timeoutMs?: number }) {\n\t\tthis.root = root;\n\t\tthis.timeoutMs = timeoutMs;\n\t}\n\n\t/** Every file the program does not ignore, relative to the root, in any order. */\n\tprotected abstract listCandidates(signal: AbortSignal): Promise<string[]>;\n\n\t/**\n\t * Hands `keep` every line matching `pattern` in a file the program does not ignore, in any\n\t * order, as the program prints it. Nothing is held here: `keep` decides what to store.\n\t */\n\tprotected abstract findMatches(\n\t\tpattern: string,\n\t\tignoreCase: boolean,\n\t\tsignal: AbortSignal,\n\t\tkeep: (match: TextMatch) => void,\n\t): Promise<void>;\n\n\tlistFiles(\n\t\tquery: { path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ files: string[]; total: number; truncated: boolean }> {\n\t\treturn this.withTimeout(signal, async bounded => {\n\t\t\tconst inScope = [...new Set(await this.listCandidates(bounded))].filter(path =>\n\t\t\t\tthis.inScope(path, query),\n\t\t\t);\n\t\t\tconst files = (await this.regularFiles(inScope)).sort(compareText);\n\n\t\t\treturn {\n\t\t\t\tfiles: files.slice(0, query.limit),\n\t\t\t\ttotal: files.length,\n\t\t\t\ttruncated: files.length > query.limit,\n\t\t\t};\n\t\t});\n\t}\n\n\tsearchText(\n\t\tquery: { pattern: string; ignoreCase: boolean; path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ matches: TextMatch[]; total: number; truncated: boolean }> {\n\t\treturn this.withTimeout(signal, async bounded => {\n\t\t\tconst listed = new Set(await this.listCandidates(bounded));\n\t\t\t// The scope applies as the output arrives, so narrowing a search is what makes it\n\t\t\t// lighter: matches outside it are never stored, however many the program prints.\n\t\t\tconst kept: TextMatch[] = [];\n\t\t\tawait this.findMatches(query.pattern, query.ignoreCase, bounded, match => {\n\t\t\t\tif (!listed.has(match.path) || !this.inScope(match.path, query)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (kept.length === maxStoredMatches) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`More than ${String(maxStoredMatches)} lines match; narrow the pattern, path or glob`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tkept.push({ ...match, text: match.text.slice(0, maxStoredLineLength) });\n\t\t\t});\n\n\t\t\t// Only the files that matched are checked on disk, not the whole repository.\n\t\t\tconst regular = new Set(await this.regularFiles([...new Set(kept.map(match => match.path))]));\n\t\t\tconst matches = kept\n\t\t\t\t.filter(match => regular.has(match.path))\n\t\t\t\t.sort((a, b) => compareText(a.path, b.path) || a.line - b.line);\n\n\t\t\treturn {\n\t\t\t\tmatches: matches.slice(0, query.limit),\n\t\t\t\ttotal: matches.length,\n\t\t\t\ttruncated: matches.length > query.limit,\n\t\t\t};\n\t\t});\n\t}\n\n\treadFile(\n\t\tquery: { path: string; fromLine: number; lineCount: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ lines: string[]; totalLines: number; truncated: boolean }> {\n\t\treturn this.withTimeout(signal, async bounded => {\n\t\t\tconst path = this.toRelative(query.path);\n\t\t\tconst visible =\n\t\t\t\tpath !== undefined &&\n\t\t\t\t(await this.listCandidates(bounded)).includes(path) &&\n\t\t\t\t(await this.regularFiles([path])).length === 1;\n\t\t\t// One answer for a missing file and an ignored one, or the model would learn the secret exists.\n\t\t\tif (!visible) {\n\t\t\t\tthrow new Error(`No such file: ${query.path}`);\n\t\t\t}\n\n\t\t\tconst absolute = join(this.root, path);\n\t\t\tif (await isBinary(absolute)) {\n\t\t\t\tthrow new Error(`Not a text file: ${query.path}`);\n\t\t\t}\n\n\t\t\t// Streamed, so a large file costs its length in lines, not in memory.\n\t\t\tconst reader = createInterface({\n\t\t\t\tinput: createReadStream(absolute, { encoding: 'utf8', signal: bounded }),\n\t\t\t\tcrlfDelay: Infinity,\n\t\t\t});\n\t\t\tconst last = query.fromLine + query.lineCount - 1;\n\t\t\tconst lines: string[] = [];\n\t\t\tlet totalLines = 0;\n\t\t\tfor await (const line of reader) {\n\t\t\t\ttotalLines++;\n\t\t\t\tif (totalLines >= query.fromLine && totalLines <= last) {\n\t\t\t\t\tlines.push(line);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { lines, totalLines, truncated: totalLines > last };\n\t\t});\n\t}\n\n\t/**\n\t * Runs an operation under the run's signal and this workspace's time limit together. The\n\t * run's cancellation escapes unchanged; the time limit becomes an error the model can act on.\n\t */\n\tprivate async withTimeout<Result>(\n\t\tsignal: AbortSignal,\n\t\toperation: (bounded: AbortSignal) => Promise<Result>,\n\t): Promise<Result> {\n\t\tsignal.throwIfAborted();\n\t\tconst timeout = AbortSignal.timeout(this.timeoutMs);\n\t\ttry {\n\t\t\treturn await operation(AbortSignal.any([signal, timeout]));\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tif (timeout.aborted) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`The operation took longer than ${String(this.timeoutMs / 1000)} s; narrow the pattern, path or glob`,\n\t\t\t\t\t{ cause: error },\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/** Regular files only: a symlink could point out of the root, and a deleted file is gone. */\n\tprivate async regularFiles(paths: string[]): Promise<string[]> {\n\t\tconst regular = await Promise.all(\n\t\t\tpaths.map(path =>\n\t\t\t\tlstat(join(this.root, path)).then(\n\t\t\t\t\tstats => stats.isFile(),\n\t\t\t\t\t() => false,\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\treturn paths.filter((_, index) => regular[index]);\n\t}\n\n\tprivate inScope(path: string, { path: scope, glob }: { path?: string; glob?: string }): boolean {\n\t\tif (scope !== undefined) {\n\t\t\tconst folder = this.toRelative(scope);\n\t\t\tif (folder === undefined) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (folder !== '' && path !== folder && !path.startsWith(`${folder}/`)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn glob === undefined || matchesGlob(path, glob);\n\t}\n\n\t/** A path relative to the root with `/`, '' for the root itself, or undefined when it leaves it. */\n\tprivate toRelative(path: string): string | undefined {\n\t\tconst relativePath = relative(this.root, resolve(this.root, path));\n\t\tif (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn relativePath.split(sep).join('/');\n\t}\n}\n\nasync function isBinary(path: string): Promise<boolean> {\n\tconst file = await open(path);\n\ttry {\n\t\tconst probe = Buffer.alloc(binaryProbeBytes);\n\t\tconst { bytesRead } = await file.read(probe, 0, probe.length, 0);\n\t\treturn probe.subarray(0, bytesRead).includes(0);\n\t} finally {\n\t\tawait file.close();\n\t}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ILogger } from '#src/shared/domain/logger';
|
|
2
|
+
import type { Workspace } from '../domain/workspace.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Picks how to read `root`: the ripgrep that ships with the package, or git when this platform
|
|
5
|
+
* has no ripgrep binary, which is logged because it is a degraded path. Plain `grep` is never a
|
|
6
|
+
* fallback: it knows nothing of `.gitignore`. See "`.gitignore` is the boundary of what an agent
|
|
7
|
+
* reads" in decisions.md.
|
|
8
|
+
*
|
|
9
|
+
* @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createWorkspace({ root, logger, }: {
|
|
12
|
+
root: string;
|
|
13
|
+
logger: ILogger;
|
|
14
|
+
}): Promise<Workspace>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { UnrecoverableError } from '#src/agent/domain/errors';
|
|
2
|
+
import { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';
|
|
3
|
+
import { GitWorkspace } from "./gitWorkspace.js";
|
|
4
|
+
import { RipgrepWorkspace } from "./ripgrepWorkspace.js";
|
|
5
|
+
import { runProcess } from "./runProcess.js";
|
|
6
|
+
// Long enough for a slow disk; short enough that a hung git does not hold up building an agent.
|
|
7
|
+
const gitCheckTimeoutMs = 5_000;
|
|
8
|
+
async function loadRipgrep() {
|
|
9
|
+
try {
|
|
10
|
+
// Dynamic: the package throws on import when the platform has no binary.
|
|
11
|
+
const { rgPath } = await import('@vscode/ripgrep');
|
|
12
|
+
return rgPath;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function isGitWorkTree(root) {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout, exitCode } = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
21
|
+
cwd: root,
|
|
22
|
+
signal: AbortSignal.timeout(gitCheckTimeoutMs),
|
|
23
|
+
});
|
|
24
|
+
return exitCode === 0 && stdout.trim() === 'true';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// No git at all, or one that never answered.
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Picks how to read `root`: the ripgrep that ships with the package, or git when this platform
|
|
33
|
+
* has no ripgrep binary, which is logged because it is a degraded path. Plain `grep` is never a
|
|
34
|
+
* fallback: it knows nothing of `.gitignore`. See "`.gitignore` is the boundary of what an agent
|
|
35
|
+
* reads" in decisions.md.
|
|
36
|
+
*
|
|
37
|
+
* @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.
|
|
38
|
+
*/
|
|
39
|
+
export async function createWorkspace({ root, logger, }) {
|
|
40
|
+
const ripgrepPath = await loadRipgrep();
|
|
41
|
+
if (ripgrepPath !== undefined) {
|
|
42
|
+
return new RipgrepWorkspace({ root, ripgrepPath });
|
|
43
|
+
}
|
|
44
|
+
if (await isGitWorkTree(root)) {
|
|
45
|
+
treatErrors(() => {
|
|
46
|
+
logger.warn('ripgrep is not available on this platform; reading the repository through git');
|
|
47
|
+
}, classifyHostFailure, 'Workspace logger failed while choosing an implementation');
|
|
48
|
+
return new GitWorkspace({ root });
|
|
49
|
+
}
|
|
50
|
+
throw new UnrecoverableError('No program can read the workspace', {
|
|
51
|
+
cause: `ripgrep is not available on this platform, and ${root} is not a git repository.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=createWorkspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/createWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAGrF,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,gGAAgG;AAChG,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,KAAK,UAAU,WAAW;IACzB,IAAI,CAAC;QACJ,yEAAyE;QACzE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE;YAC5F,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,iBAAiB,CAAC;SAC9C,CAAC,CAAC;QACH,OAAO,QAAQ,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;QAC7C,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EACrC,IAAI,EACJ,MAAM,GAIN;IACA,MAAM,WAAW,GAAG,MAAM,WAAW,EAAE,CAAC;IACxC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,gBAAgB,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,WAAW,CACV,GAAG,EAAE;YACJ,MAAM,CAAC,IAAI,CACV,+EAA+E,CAC/E,CAAC;QACH,CAAC,EACD,mBAAmB,EACnB,0DAA0D,CAC1D,CAAC;QACF,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,IAAI,kBAAkB,CAAC,mCAAmC,EAAE;QACjE,KAAK,EAAE,kDAAkD,IAAI,2BAA2B;KACxF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { UnrecoverableError } from '#src/agent/domain/errors';\nimport { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { GitWorkspace } from './gitWorkspace.ts';\nimport { RipgrepWorkspace } from './ripgrepWorkspace.ts';\nimport { runProcess } from './runProcess.ts';\n\n// Long enough for a slow disk; short enough that a hung git does not hold up building an agent.\nconst gitCheckTimeoutMs = 5_000;\n\nasync function loadRipgrep(): Promise<string | undefined> {\n\ttry {\n\t\t// Dynamic: the package throws on import when the platform has no binary.\n\t\tconst { rgPath } = await import('@vscode/ripgrep');\n\t\treturn rgPath;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function isGitWorkTree(root: string): Promise<boolean> {\n\ttry {\n\t\tconst { stdout, exitCode } = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], {\n\t\t\tcwd: root,\n\t\t\tsignal: AbortSignal.timeout(gitCheckTimeoutMs),\n\t\t});\n\t\treturn exitCode === 0 && stdout.trim() === 'true';\n\t} catch {\n\t\t// No git at all, or one that never answered.\n\t\treturn false;\n\t}\n}\n\n/**\n * Picks how to read `root`: the ripgrep that ships with the package, or git when this platform\n * has no ripgrep binary, which is logged because it is a degraded path. Plain `grep` is never a\n * fallback: it knows nothing of `.gitignore`. See \"`.gitignore` is the boundary of what an agent\n * reads\" in decisions.md.\n *\n * @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.\n */\nexport async function createWorkspace({\n\troot,\n\tlogger,\n}: {\n\troot: string;\n\tlogger: ILogger;\n}): Promise<Workspace> {\n\tconst ripgrepPath = await loadRipgrep();\n\tif (ripgrepPath !== undefined) {\n\t\treturn new RipgrepWorkspace({ root, ripgrepPath });\n\t}\n\n\tif (await isGitWorkTree(root)) {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'ripgrep is not available on this platform; reading the repository through git',\n\t\t\t\t);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Workspace logger failed while choosing an implementation',\n\t\t);\n\t\treturn new GitWorkspace({ root });\n\t}\n\n\tthrow new UnrecoverableError('No program can read the workspace', {\n\t\tcause: `ripgrep is not available on this platform, and ${root} is not a git repository.`,\n\t});\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Tool } from '../domain/tool.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the
|
|
5
|
+
* model sends, and types the input `execute` receives. Invalid input is rejected with a message
|
|
6
|
+
* naming what is wrong, which the agent hands back to the model as an error result.
|
|
7
|
+
*
|
|
8
|
+
* @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.
|
|
9
|
+
*/
|
|
10
|
+
export declare function defineTool<Schema extends z.ZodObject>({ name, description, input, execute, }: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
input: Schema;
|
|
14
|
+
execute: (input: z.infer<Schema>, signal: AbortSignal) => Promise<string>;
|
|
15
|
+
}): Tool;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InvalidAgentConfigError } from '#src/agent/domain/errors';
|
|
3
|
+
// Both providers take tools in strict mode, which supports only part of JSON Schema. Claude
|
|
4
|
+
// rejects these bounds; a tool that needs one checks it in its own code instead.
|
|
5
|
+
const unsupportedKeywords = [
|
|
6
|
+
'minimum',
|
|
7
|
+
'maximum',
|
|
8
|
+
'exclusiveMinimum',
|
|
9
|
+
'exclusiveMaximum',
|
|
10
|
+
'multipleOf',
|
|
11
|
+
'minLength',
|
|
12
|
+
'maxLength',
|
|
13
|
+
];
|
|
14
|
+
// Keywords whose value is one schema, or a list or map of them. Anything else, such as the
|
|
15
|
+
// keys of `properties`, is a name or a value, never a schema to check.
|
|
16
|
+
const schemaKeywords = ['items', 'not', 'additionalProperties'];
|
|
17
|
+
const schemaListKeywords = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];
|
|
18
|
+
const schemaMapKeywords = ['properties', '$defs', 'definitions'];
|
|
19
|
+
function isSchema(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks the whole schema, nested objects included, and visits only schemas: a property named
|
|
24
|
+
* `maxLength` is a name, not the keyword. A schema the providers reject would fail every
|
|
25
|
+
* request with a 400, so it fails here, when the tool is defined.
|
|
26
|
+
*/
|
|
27
|
+
function assertStrict(schema, toolName) {
|
|
28
|
+
if (!isSchema(schema)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const keyword = unsupportedKeywords.find(candidate => candidate in schema);
|
|
32
|
+
if (keyword) {
|
|
33
|
+
throw new InvalidAgentConfigError(`Tool "${toolName}" uses "${keyword}", which strict mode does not support; check it in the tool's code`);
|
|
34
|
+
}
|
|
35
|
+
if (schema.type === 'object') {
|
|
36
|
+
// A record or a loose object takes keys strict mode cannot know in advance.
|
|
37
|
+
if (schema.additionalProperties !== false) {
|
|
38
|
+
throw new InvalidAgentConfigError(`Tool "${toolName}" has an object open to any key; strict mode needs every key declared`);
|
|
39
|
+
}
|
|
40
|
+
const required = (schema.required ?? []);
|
|
41
|
+
// OpenAI requires every property: an optional one is a union with `null`.
|
|
42
|
+
const optional = Object.keys(schema.properties ?? {}).filter(property => !required.includes(property));
|
|
43
|
+
if (optional.length > 0) {
|
|
44
|
+
throw new InvalidAgentConfigError(`Tool "${toolName}" has optional fields (${optional.join(', ')}); make them .nullable() instead`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const key of schemaKeywords)
|
|
48
|
+
assertStrict(schema[key], toolName);
|
|
49
|
+
for (const key of schemaListKeywords) {
|
|
50
|
+
const list = schema[key];
|
|
51
|
+
if (Array.isArray(list))
|
|
52
|
+
for (const item of list)
|
|
53
|
+
assertStrict(item, toolName);
|
|
54
|
+
}
|
|
55
|
+
for (const key of schemaMapKeywords) {
|
|
56
|
+
const map = schema[key];
|
|
57
|
+
if (isSchema(map))
|
|
58
|
+
for (const item of Object.values(map))
|
|
59
|
+
assertStrict(item, toolName);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the
|
|
64
|
+
* model sends, and types the input `execute` receives. Invalid input is rejected with a message
|
|
65
|
+
* naming what is wrong, which the agent hands back to the model as an error result.
|
|
66
|
+
*
|
|
67
|
+
* @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.
|
|
68
|
+
*/
|
|
69
|
+
export function defineTool({ name, description, input, execute, }) {
|
|
70
|
+
const inputSchema = { ...z.toJSONSchema(input) };
|
|
71
|
+
// The dialect marker is noise to both APIs.
|
|
72
|
+
delete inputSchema.$schema;
|
|
73
|
+
assertStrict(inputSchema, name);
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
description,
|
|
77
|
+
inputSchema: inputSchema,
|
|
78
|
+
execute: (raw, signal) => {
|
|
79
|
+
const parsed = input.safeParse(raw);
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
return Promise.reject(new Error(z.prettifyError(parsed.error)));
|
|
82
|
+
}
|
|
83
|
+
return execute(parsed.data, signal);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=defineTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/defineTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport { InvalidAgentConfigError } from '#src/agent/domain/errors';\nimport type { JSONSchema } from '#src/llm/domain/tool';\nimport type { Tool } from '../domain/tool.ts';\n\n// Both providers take tools in strict mode, which supports only part of JSON Schema. Claude\n// rejects these bounds; a tool that needs one checks it in its own code instead.\nconst unsupportedKeywords = [\n\t'minimum',\n\t'maximum',\n\t'exclusiveMinimum',\n\t'exclusiveMaximum',\n\t'multipleOf',\n\t'minLength',\n\t'maxLength',\n];\n\n// Keywords whose value is one schema, or a list or map of them. Anything else, such as the\n// keys of `properties`, is a name or a value, never a schema to check.\nconst schemaKeywords = ['items', 'not', 'additionalProperties'];\nconst schemaListKeywords = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];\nconst schemaMapKeywords = ['properties', '$defs', 'definitions'];\n\nfunction isSchema(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Walks the whole schema, nested objects included, and visits only schemas: a property named\n * `maxLength` is a name, not the keyword. A schema the providers reject would fail every\n * request with a 400, so it fails here, when the tool is defined.\n */\nfunction assertStrict(schema: unknown, toolName: string): void {\n\tif (!isSchema(schema)) {\n\t\treturn;\n\t}\n\n\tconst keyword = unsupportedKeywords.find(candidate => candidate in schema);\n\tif (keyword) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`Tool \"${toolName}\" uses \"${keyword}\", which strict mode does not support; check it in the tool's code`,\n\t\t);\n\t}\n\n\tif (schema.type === 'object') {\n\t\t// A record or a loose object takes keys strict mode cannot know in advance.\n\t\tif (schema.additionalProperties !== false) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has an object open to any key; strict mode needs every key declared`,\n\t\t\t);\n\t\t}\n\n\t\tconst required = (schema.required ?? []) as string[];\n\t\t// OpenAI requires every property: an optional one is a union with `null`.\n\t\tconst optional = Object.keys(schema.properties ?? {}).filter(\n\t\t\tproperty => !required.includes(property),\n\t\t);\n\t\tif (optional.length > 0) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has optional fields (${optional.join(', ')}); make them .nullable() instead`,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (const key of schemaKeywords) assertStrict(schema[key], toolName);\n\tfor (const key of schemaListKeywords) {\n\t\tconst list = schema[key];\n\t\tif (Array.isArray(list)) for (const item of list) assertStrict(item, toolName);\n\t}\n\tfor (const key of schemaMapKeywords) {\n\t\tconst map = schema[key];\n\t\tif (isSchema(map)) for (const item of Object.values(map)) assertStrict(item, toolName);\n\t}\n}\n\n/**\n * Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the\n * model sends, and types the input `execute` receives. Invalid input is rejected with a message\n * naming what is wrong, which the agent hands back to the model as an error result.\n *\n * @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.\n */\nexport function defineTool<Schema extends z.ZodObject>({\n\tname,\n\tdescription,\n\tinput,\n\texecute,\n}: {\n\tname: string;\n\tdescription: string;\n\tinput: Schema;\n\texecute: (input: z.infer<Schema>, signal: AbortSignal) => Promise<string>;\n}): Tool {\n\tconst inputSchema: Record<string, unknown> = { ...z.toJSONSchema(input) };\n\t// The dialect marker is noise to both APIs.\n\tdelete inputSchema.$schema;\n\tassertStrict(inputSchema, name);\n\n\treturn {\n\t\tname,\n\t\tdescription,\n\t\tinputSchema: inputSchema as JSONSchema,\n\t\texecute: (raw, signal) => {\n\t\t\tconst parsed = input.safeParse(raw);\n\t\t\tif (!parsed.success) {\n\t\t\t\treturn Promise.reject(new Error(z.prettifyError(parsed.error)));\n\t\t\t}\n\n\t\t\treturn execute(parsed.data, signal);\n\t\t},\n\t};\n}\n"]}
|