diff --git a/apps/cli/package.json b/apps/cli/package.json index 0328635c..deedf1a3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -48,8 +48,8 @@ "dependencies": { "@fastify/multipart": "^10.0.0", "@fastify/static": "^9.1.3", - "@earendil-works/pi-agent-core": "0.83.0", - "@earendil-works/pi-ai": "0.83.0", + "@earendil-works/pi-agent-core": "0.84.2", + "@earendil-works/pi-ai": "0.84.2", "@modelcontextprotocol/sdk": "^1.12.1", "better-sqlite3": "^12.4.6", "commander": "^13.1.0", diff --git a/packages/node-runtime/package.json b/packages/node-runtime/package.json index 6e76da08..8589454a 100644 --- a/packages/node-runtime/package.json +++ b/packages/node-runtime/package.json @@ -15,8 +15,8 @@ "./src/*": "./src/*.ts" }, "dependencies": { - "@earendil-works/pi-agent-core": "0.83.0", - "@earendil-works/pi-ai": "0.83.0", + "@earendil-works/pi-agent-core": "0.84.2", + "@earendil-works/pi-ai": "0.84.2", "@huggingface/transformers": "^4.2.0", "@openchatlab/config": "workspace:*", "@openchatlab/core": "workspace:*", diff --git a/packages/node-runtime/src/ai/agent/__tests__/core.test.ts b/packages/node-runtime/src/ai/agent/__tests__/core.test.ts index 51a7a77f..18490044 100644 --- a/packages/node-runtime/src/ai/agent/__tests__/core.test.ts +++ b/packages/node-runtime/src/ai/agent/__tests__/core.test.ts @@ -359,6 +359,93 @@ describe('runAgentCore runtime contract', () => { ) }) + it('stops after one final-answer turn when the tool round limit is reached', async () => { + let providerCalls = 0 + let handlerCalls = 0 + const tool = createTool('lookup', async () => { + handlerCalls += 1 + return { content: [{ type: 'text', text: 'tool result' }], details: null } + }) + const toolCall = () => + assistantMessage([{ type: 'toolCall', id: `tool-${providerCalls}`, name: 'lookup', arguments: {} }], { + stopReason: 'toolUse', + }) + + const result = await runAgentCore( + createOptions({ + tools: [tool], + maxToolRounds: 1, + steerMessage: 'Finish now.', + streamFn: createScriptedStream([ + () => { + providerCalls += 1 + return toolCall() + }, + (context) => { + providerCalls += 1 + assert.deepEqual(context.tools, []) + return toolCall() + }, + () => { + providerCalls += 1 + return toolCall() + }, + () => { + providerCalls += 1 + return assistantMessage([{ type: 'text', text: 'Too late' }]) + }, + ]), + }) + ) + + assert.deepEqual( + { providerCalls, handlerCalls, toolRounds: result.toolRounds }, + { providerCalls: 2, handlerCalls: 1, toolRounds: 1 } + ) + }) + + it('stops after the first turn when the tool round limit is zero', async () => { + let providerCalls = 0 + let handlerCalls = 0 + const tool = createTool('lookup', async () => { + handlerCalls += 1 + return { content: [{ type: 'text', text: 'tool result' }], details: null } + }) + const toolCall = () => + assistantMessage([{ type: 'toolCall', id: `tool-${providerCalls}`, name: 'lookup', arguments: {} }], { + stopReason: 'toolUse', + }) + + const result = await runAgentCore( + createOptions({ + tools: [tool], + maxToolRounds: 0, + streamFn: createScriptedStream([ + (context) => { + providerCalls += 1 + assert.deepEqual(context.tools, []) + return toolCall() + }, + () => { + providerCalls += 1 + return toolCall() + }, + ]), + }) + ) + + assert.deepEqual( + { providerCalls, handlerCalls, toolRounds: result.toolRounds }, + { providerCalls: 1, handlerCalls: 0, toolRounds: 0 } + ) + assert.deepEqual( + result.finalMessages.map((message) => message.role), + ['user', 'assistant', 'toolResult'] + ) + const finalMessage = result.finalMessages.at(-1) + assert.equal(finalMessage?.role === 'toolResult' && finalMessage.isError, true) + }) + it('forwards the stable AI conversation id as the provider session id', async () => { let capturedOptions: SimpleStreamOptions | undefined diff --git a/packages/node-runtime/src/ai/agent/core.ts b/packages/node-runtime/src/ai/agent/core.ts index 2633ae16..131a676d 100644 --- a/packages/node-runtime/src/ai/agent/core.ts +++ b/packages/node-runtime/src/ai/agent/core.ts @@ -96,7 +96,8 @@ export async function runAgentCore(options: AgentCoreOptions): Promise (result as { isError?: boolean }).isError === true ? { isError: true } : undefined, + shouldStopAfterTurn: async () => hasCompletedFinalAnswerTurn, prepareNextTurnWithContext: ({ context }) => hasReachedToolRoundLimit ? { @@ -219,9 +221,10 @@ export async function runAgentCore(options: AgentCoreOptions): Promise 0 - if (hadToolCalls) { + const isFinalAnswerTurn = hasReachedToolRoundLimit + if (hadToolCalls && !hasReachedToolRoundLimit) { toolRounds += 1 - if (!hasReachedToolRoundLimit && maxToolRounds > 0 && toolRounds >= maxToolRounds) { + if (maxToolRounds > 0 && toolRounds >= maxToolRounds) { hasReachedToolRoundLimit = true coreAgent.state.tools = [] coreAgent.steer({ @@ -231,6 +234,7 @@ export async function runAgentCore(options: AgentCoreOptions): Promise=22.19.0'} - '@earendil-works/pi-ai@0.83.0': - resolution: {integrity: sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ==} + '@earendil-works/pi-ai@0.84.2': + resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==} engines: {node: '>=22.19.0'} hasBin: true + '@earendil-works/pi-telemetry@0.84.3': + resolution: {integrity: sha512-sgEkWoKrvSGaKn+YfLLFZmn+/A7B/w62eLwTD57nI+C9to8ITlFFVbgC2OtwvPnT3NFGHdCd53qhBEMIlptD1g==} + engines: {node: '>=22.19.0'} + '@electron-toolkit/preload@3.0.2': resolution: {integrity: sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==} peerDependencies: @@ -1825,14 +1829,6 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} - '@mistralai/mistralai@2.2.6': - resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -2447,10 +2443,6 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/semantic-conventions@1.43.0': - resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} - engines: {node: '>=14'} - '@peculiar/asn1-schema@2.9.0': resolution: {integrity: sha512-AKvPMOM7LfK0uFe1m7o7+veOa8xQGPqsqOrKi3QKgCElzwjGp39mbhr2g7mt/v/mXQHiIvJmDj5cJS173x8Q9Q==} @@ -5692,9 +5684,8 @@ packages: onnxruntime-web@1.26.0-dev.20260416-b7804b056c: resolution: {integrity: sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==} - openai@6.26.0: - resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} - hasBin: true + openai@6.40.0: + resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} peerDependencies: ws: ^8.18.0 zod: ^3.25 || ^4.0 @@ -7677,9 +7668,10 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76)': + '@earendil-works/pi-agent-core@0.84.2(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76)': dependencies: - '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-telemetry': 0.84.3 diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -7692,17 +7684,17 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76)': + '@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0)(ws@8.19.0)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@earendil-works/pi-telemetry': 0.84.3 '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76))(supports-color@7.2.0) - '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2(supports-color@7.2.0) https-proxy-agent: 7.0.6(supports-color@7.2.0) - openai: 6.26.0(ws@8.19.0)(zod@3.25.76) + openai: 6.40.0(ws@8.19.0)(zod@3.25.76) partial-json: 0.1.7 typebox: 1.3.7 transitivePeerDependencies: @@ -7713,6 +7705,8 @@ snapshots: - ws - zod + '@earendil-works/pi-telemetry@0.84.3': {} + '@electron-toolkit/preload@3.0.2(electron@35.7.5(supports-color@7.2.0))': dependencies: electron: 35.7.5(supports-color@7.2.0) @@ -8532,18 +8526,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/semantic-conventions': 1.43.0 - ws: 8.19.0 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - optionalDependencies: - '@opentelemetry/api': 1.9.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@modelcontextprotocol/sdk@1.29.0(supports-color@7.2.0)(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.18) @@ -9221,8 +9203,6 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@opentelemetry/semantic-conventions@1.43.0': {} - '@peculiar/asn1-schema@2.9.0': dependencies: '@peculiar/utils': 2.0.3 @@ -12563,7 +12543,7 @@ snapshots: platform: 1.3.6 protobufjs: 7.5.4 - openai@6.26.0(ws@8.19.0)(zod@3.25.76): + openai@6.40.0(ws@8.19.0)(zod@3.25.76): optionalDependencies: ws: 8.19.0 zod: 3.25.76