@renxqoo/renx-code 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/bin/renx.cjs +16 -0
  2. package/package.json +2 -45
  3. package/src/agent/runtime/runtime.context-usage.test.ts +4 -5
  4. package/src/agent/runtime/runtime.error-handling.test.ts +4 -5
  5. package/src/agent/runtime/runtime.test.ts +7 -4
  6. package/src/agent/runtime/runtime.ts +3 -9
  7. package/src/agent/runtime/runtime.usage-forwarding.test.ts +4 -5
  8. package/src/agent/runtime/source-modules.test.ts +16 -35
  9. package/src/agent/runtime/source-modules.ts +17 -0
  10. package/vendor/agent-root/src/agent/ENTERPRISE_ACCEPTANCE_CHECKLIST.md +95 -0
  11. package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.html +1345 -0
  12. package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.md +1353 -0
  13. package/vendor/agent-root/src/agent/ERROR_CONTRACT.md +60 -0
  14. package/vendor/agent-root/src/agent/TEST_COVERAGE_ANALYSIS.md +278 -0
  15. package/vendor/agent-root/src/agent/__test__/error-contract.test.ts +72 -0
  16. package/vendor/agent-root/src/agent/__test__/types.test.ts +137 -0
  17. package/vendor/agent-root/src/agent/agent/__test__/abort-runtime.test.ts +83 -0
  18. package/vendor/agent-root/src/agent/agent/__test__/callback-safety.test.ts +34 -0
  19. package/vendor/agent-root/src/agent/agent/__test__/compaction.test.ts +323 -0
  20. package/vendor/agent-root/src/agent/agent/__test__/concurrency.test.ts +290 -0
  21. package/vendor/agent-root/src/agent/agent/__test__/error-normalizer.test.ts +377 -0
  22. package/vendor/agent-root/src/agent/agent/__test__/error.test.ts +212 -0
  23. package/vendor/agent-root/src/agent/agent/__test__/fault-injection.test.ts +295 -0
  24. package/vendor/agent-root/src/agent/agent/__test__/index.test.ts +3607 -0
  25. package/vendor/agent-root/src/agent/agent/__test__/logger.test.ts +35 -0
  26. package/vendor/agent-root/src/agent/agent/__test__/message-utils.test.ts +517 -0
  27. package/vendor/agent-root/src/agent/agent/__test__/telemetry.test.ts +97 -0
  28. package/vendor/agent-root/src/agent/agent/__test__/timeout-budget.test.ts +479 -0
  29. package/vendor/agent-root/src/agent/agent/__test__/tool-call-merge.test.ts +80 -0
  30. package/vendor/agent-root/src/agent/agent/__test__/tool-execution-ledger.test.ts +76 -0
  31. package/vendor/agent-root/src/agent/agent/__test__/write-buffer.test.ts +173 -0
  32. package/vendor/agent-root/src/agent/agent/__test__/write-file-session.test.ts +109 -0
  33. package/vendor/agent-root/src/agent/agent/abort-runtime.ts +71 -0
  34. package/vendor/agent-root/src/agent/agent/callback-safety.ts +33 -0
  35. package/vendor/agent-root/src/agent/agent/compaction.ts +291 -0
  36. package/vendor/agent-root/src/agent/agent/concurrency.ts +103 -0
  37. package/vendor/agent-root/src/agent/agent/error-normalizer.ts +190 -0
  38. package/vendor/agent-root/src/agent/agent/error.ts +198 -0
  39. package/vendor/agent-root/src/agent/agent/index.ts +1772 -0
  40. package/vendor/agent-root/src/agent/agent/logger.ts +65 -0
  41. package/vendor/agent-root/src/agent/agent/message-utils.ts +101 -0
  42. package/vendor/agent-root/src/agent/agent/stream-events.ts +61 -0
  43. package/vendor/agent-root/src/agent/agent/telemetry.ts +123 -0
  44. package/vendor/agent-root/src/agent/agent/timeout-budget.ts +227 -0
  45. package/vendor/agent-root/src/agent/agent/tool-call-merge.ts +111 -0
  46. package/vendor/agent-root/src/agent/agent/tool-execution-ledger.ts +164 -0
  47. package/vendor/agent-root/src/agent/agent/write-buffer.ts +188 -0
  48. package/vendor/agent-root/src/agent/agent/write-file-session.ts +238 -0
  49. package/vendor/agent-root/src/agent/app/__test__/agent-app-service.test.ts +1053 -0
  50. package/vendor/agent-root/src/agent/app/__test__/minimal-agent-application.test.ts +158 -0
  51. package/vendor/agent-root/src/agent/app/__test__/sqlite-agent-app-store.test.ts +437 -0
  52. package/vendor/agent-root/src/agent/app/agent-app-service.ts +748 -0
  53. package/vendor/agent-root/src/agent/app/contracts.ts +109 -0
  54. package/vendor/agent-root/src/agent/app/index.ts +5 -0
  55. package/vendor/agent-root/src/agent/app/minimal-agent-application.ts +151 -0
  56. package/vendor/agent-root/src/agent/app/ports.ts +72 -0
  57. package/vendor/agent-root/src/agent/app/sqlite-agent-app-store.ts +1182 -0
  58. package/vendor/agent-root/src/agent/app/sqlite-client.ts +177 -0
  59. package/vendor/agent-root/src/agent/docs/cli-app-layer/00-README.md +36 -0
  60. package/vendor/agent-root/src/agent/docs/cli-app-layer/01-scope-and-goals.md +33 -0
  61. package/vendor/agent-root/src/agent/docs/cli-app-layer/02-architecture-overview.md +40 -0
  62. package/vendor/agent-root/src/agent/docs/cli-app-layer/03-domain-model-and-contracts.md +91 -0
  63. package/vendor/agent-root/src/agent/docs/cli-app-layer/04-ports-and-interfaces.md +116 -0
  64. package/vendor/agent-root/src/agent/docs/cli-app-layer/05-run-orchestration-and-state-machine.md +52 -0
  65. package/vendor/agent-root/src/agent/docs/cli-app-layer/06-cli-commands-and-ux.md +53 -0
  66. package/vendor/agent-root/src/agent/docs/cli-app-layer/07-storage-design-local.md +52 -0
  67. package/vendor/agent-root/src/agent/docs/cli-app-layer/08-error-and-observability.md +40 -0
  68. package/vendor/agent-root/src/agent/docs/cli-app-layer/09-security-and-policy-boundary.md +19 -0
  69. package/vendor/agent-root/src/agent/docs/cli-app-layer/10-test-plan-and-acceptance.md +28 -0
  70. package/vendor/agent-root/src/agent/docs/cli-app-layer/11-implementation-phases.md +26 -0
  71. package/vendor/agent-root/src/agent/docs/cli-app-layer/12-open-questions-and-risks.md +30 -0
  72. package/vendor/agent-root/src/agent/docs/cli-app-layer/13-sqlite-schema-fields-and-rationale.md +567 -0
  73. package/vendor/agent-root/src/agent/docs/cli-app-layer/14-project-flow-mermaid.md +583 -0
  74. package/vendor/agent-root/src/agent/docs/cli-app-layer/15-openclaw-style-project-blueprint.md +972 -0
  75. package/vendor/agent-root/src/agent/error-contract.ts +154 -0
  76. package/vendor/agent-root/src/agent/prompts/system.ts +246 -0
  77. package/vendor/agent-root/src/agent/prompts/system1.ts +208 -0
  78. package/vendor/agent-root/src/agent/storage/__test__/file-history-store.test.ts +98 -0
  79. package/vendor/agent-root/src/agent/storage/file-history-store.ts +313 -0
  80. package/vendor/agent-root/src/agent/storage/file-storage-config.ts +94 -0
  81. package/vendor/agent-root/src/agent/storage/file-system.ts +31 -0
  82. package/vendor/agent-root/src/agent/storage/file-write-service.ts +21 -0
  83. package/vendor/agent-root/src/agent/tool/__test__/base-tool.test.ts +413 -0
  84. package/vendor/agent-root/src/agent/tool/__test__/bash-policy.test.ts +356 -0
  85. package/vendor/agent-root/src/agent/tool/__test__/bash.mocked-coverage.test.ts +375 -0
  86. package/vendor/agent-root/src/agent/tool/__test__/bash.test.ts +372 -0
  87. package/vendor/agent-root/src/agent/tool/__test__/error.test.ts +108 -0
  88. package/vendor/agent-root/src/agent/tool/__test__/file-edit-tool.test.ts +258 -0
  89. package/vendor/agent-root/src/agent/tool/__test__/file-history-tools.test.ts +121 -0
  90. package/vendor/agent-root/src/agent/tool/__test__/file-read-tool.test.ts +210 -0
  91. package/vendor/agent-root/src/agent/tool/__test__/glob.test.ts +139 -0
  92. package/vendor/agent-root/src/agent/tool/__test__/grep.mocked-coverage.test.ts +456 -0
  93. package/vendor/agent-root/src/agent/tool/__test__/grep.test.ts +192 -0
  94. package/vendor/agent-root/src/agent/tool/__test__/lsp.test.ts +300 -0
  95. package/vendor/agent-root/src/agent/tool/__test__/outside-workspace-confirmation.test.ts +214 -0
  96. package/vendor/agent-root/src/agent/tool/__test__/path-security.test.ts +336 -0
  97. package/vendor/agent-root/src/agent/tool/__test__/skill-loader.test.ts +494 -0
  98. package/vendor/agent-root/src/agent/tool/__test__/skill-parser.test.ts +543 -0
  99. package/vendor/agent-root/src/agent/tool/__test__/skill-tool.test.ts +172 -0
  100. package/vendor/agent-root/src/agent/tool/__test__/task-concurrency-and-version.test.ts +116 -0
  101. package/vendor/agent-root/src/agent/tool/__test__/task-create-get-list-update.test.ts +267 -0
  102. package/vendor/agent-root/src/agent/tool/__test__/task-create.test.ts +519 -0
  103. package/vendor/agent-root/src/agent/tool/__test__/task-errors.test.ts +225 -0
  104. package/vendor/agent-root/src/agent/tool/__test__/task-output-blocking.test.ts +223 -0
  105. package/vendor/agent-root/src/agent/tool/__test__/task-output.test.ts +184 -0
  106. package/vendor/agent-root/src/agent/tool/__test__/task-parent-abort.test.ts +287 -0
  107. package/vendor/agent-root/src/agent/tool/__test__/task-real-runner-adapter.test.ts +190 -0
  108. package/vendor/agent-root/src/agent/tool/__test__/task-run-lifecycle.test.ts +352 -0
  109. package/vendor/agent-root/src/agent/tool/__test__/task-store-runner-branches.test.ts +395 -0
  110. package/vendor/agent-root/src/agent/tool/__test__/task-store.test.ts +391 -0
  111. package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config-integration.test.ts +176 -0
  112. package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config.test.ts +68 -0
  113. package/vendor/agent-root/src/agent/tool/__test__/task-tools-core-edges.test.ts +630 -0
  114. package/vendor/agent-root/src/agent/tool/__test__/task-tools-runtime-edges.test.ts +732 -0
  115. package/vendor/agent-root/src/agent/tool/__test__/task-types.test.ts +494 -0
  116. package/vendor/agent-root/src/agent/tool/__test__/task-utils-branches.test.ts +175 -0
  117. package/vendor/agent-root/src/agent/tool/__test__/tool-manager.test.ts +505 -0
  118. package/vendor/agent-root/src/agent/tool/__test__/types.test.ts +55 -0
  119. package/vendor/agent-root/src/agent/tool/__test__/web-fetch.test.ts +244 -0
  120. package/vendor/agent-root/src/agent/tool/__test__/web-search.test.ts +290 -0
  121. package/vendor/agent-root/src/agent/tool/__test__/write-file.test.ts +368 -0
  122. package/vendor/agent-root/src/agent/tool/base-tool.ts +345 -0
  123. package/vendor/agent-root/src/agent/tool/bash-policy.ts +636 -0
  124. package/vendor/agent-root/src/agent/tool/bash.ts +688 -0
  125. package/vendor/agent-root/src/agent/tool/error.ts +131 -0
  126. package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +264 -0
  127. package/vendor/agent-root/src/agent/tool/file-history-list.ts +103 -0
  128. package/vendor/agent-root/src/agent/tool/file-history-restore.ts +149 -0
  129. package/vendor/agent-root/src/agent/tool/file-read-tool.ts +211 -0
  130. package/vendor/agent-root/src/agent/tool/glob.ts +171 -0
  131. package/vendor/agent-root/src/agent/tool/grep.ts +496 -0
  132. package/vendor/agent-root/src/agent/tool/lsp.ts +481 -0
  133. package/vendor/agent-root/src/agent/tool/path-security.ts +117 -0
  134. package/vendor/agent-root/src/agent/tool/search/common.ts +153 -0
  135. package/vendor/agent-root/src/agent/tool/skill/index.ts +13 -0
  136. package/vendor/agent-root/src/agent/tool/skill/loader.ts +229 -0
  137. package/vendor/agent-root/src/agent/tool/skill/parser.ts +124 -0
  138. package/vendor/agent-root/src/agent/tool/skill/types.ts +27 -0
  139. package/vendor/agent-root/src/agent/tool/skill-tool.ts +143 -0
  140. package/vendor/agent-root/src/agent/tool/task-create.ts +186 -0
  141. package/vendor/agent-root/src/agent/tool/task-errors.ts +42 -0
  142. package/vendor/agent-root/src/agent/tool/task-get.ts +116 -0
  143. package/vendor/agent-root/src/agent/tool/task-graph.ts +78 -0
  144. package/vendor/agent-root/src/agent/tool/task-list.ts +141 -0
  145. package/vendor/agent-root/src/agent/tool/task-mock-runner-adapter.ts +232 -0
  146. package/vendor/agent-root/src/agent/tool/task-output.ts +223 -0
  147. package/vendor/agent-root/src/agent/tool/task-parent-abort.ts +115 -0
  148. package/vendor/agent-root/src/agent/tool/task-real-runner-adapter.ts +336 -0
  149. package/vendor/agent-root/src/agent/tool/task-runner-adapter.ts +55 -0
  150. package/vendor/agent-root/src/agent/tool/task-stop.ts +187 -0
  151. package/vendor/agent-root/src/agent/tool/task-store.ts +217 -0
  152. package/vendor/agent-root/src/agent/tool/task-subagent-config.ts +149 -0
  153. package/vendor/agent-root/src/agent/tool/task-types.ts +264 -0
  154. package/vendor/agent-root/src/agent/tool/task-update.ts +315 -0
  155. package/vendor/agent-root/src/agent/tool/task.ts +209 -0
  156. package/vendor/agent-root/src/agent/tool/tool-manager.ts +362 -0
  157. package/vendor/agent-root/src/agent/tool/tool-prompts.ts +242 -0
  158. package/vendor/agent-root/src/agent/tool/types.ts +116 -0
  159. package/vendor/agent-root/src/agent/tool/web-fetch.ts +227 -0
  160. package/vendor/agent-root/src/agent/tool/web-search.ts +208 -0
  161. package/vendor/agent-root/src/agent/tool/write-file.ts +497 -0
  162. package/vendor/agent-root/src/agent/types.ts +232 -0
  163. package/vendor/agent-root/src/agent/utils/__tests__/index.test.ts +18 -0
  164. package/vendor/agent-root/src/agent/utils/__tests__/message-utils.test.ts +610 -0
  165. package/vendor/agent-root/src/agent/utils/__tests__/message.test.ts +223 -0
  166. package/vendor/agent-root/src/agent/utils/__tests__/token.test.ts +42 -0
  167. package/vendor/agent-root/src/agent/utils/index.ts +16 -0
  168. package/vendor/agent-root/src/agent/utils/message.ts +171 -0
  169. package/vendor/agent-root/src/agent/utils/token.ts +28 -0
  170. package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +129 -0
  171. package/vendor/agent-root/src/config/__tests__/loader.test.ts +247 -0
  172. package/vendor/agent-root/src/config/__tests__/runtime.test.ts +88 -0
  173. package/vendor/agent-root/src/config/index.ts +54 -0
  174. package/vendor/agent-root/src/config/loader.ts +431 -0
  175. package/vendor/agent-root/src/config/paths.ts +30 -0
  176. package/vendor/agent-root/src/config/runtime.ts +163 -0
  177. package/vendor/agent-root/src/config/types.ts +70 -0
  178. package/vendor/agent-root/src/logger/index.ts +57 -0
  179. package/vendor/agent-root/src/logger/logger.ts +819 -0
  180. package/vendor/agent-root/src/logger/types.ts +150 -0
  181. package/vendor/agent-root/src/providers/__tests__/errors.test.ts +441 -0
  182. package/vendor/agent-root/src/providers/__tests__/index.test.ts +16 -0
  183. package/vendor/agent-root/src/providers/__tests__/openai-compatible.options.test.ts +318 -0
  184. package/vendor/agent-root/src/providers/__tests__/openai-compatible.test.ts +600 -0
  185. package/vendor/agent-root/src/providers/__tests__/registry.test.ts +449 -0
  186. package/vendor/agent-root/src/providers/__tests__/responses-adapter.test.ts +298 -0
  187. package/vendor/agent-root/src/providers/adapters/__tests__/anthropic.test.ts +354 -0
  188. package/vendor/agent-root/src/providers/adapters/__tests__/kimi.test.ts +58 -0
  189. package/vendor/agent-root/src/providers/adapters/__tests__/standard.test.ts +261 -0
  190. package/vendor/agent-root/src/providers/adapters/anthropic.ts +572 -0
  191. package/vendor/agent-root/src/providers/adapters/base.ts +131 -0
  192. package/vendor/agent-root/src/providers/adapters/kimi.ts +48 -0
  193. package/vendor/agent-root/src/providers/adapters/responses.ts +732 -0
  194. package/vendor/agent-root/src/providers/adapters/standard.ts +120 -0
  195. package/vendor/agent-root/src/providers/http/__tests__/client.timeout.test.ts +313 -0
  196. package/vendor/agent-root/src/providers/http/client.ts +289 -0
  197. package/vendor/agent-root/src/providers/http/stream-parser.ts +109 -0
  198. package/vendor/agent-root/src/providers/index.ts +76 -0
  199. package/vendor/agent-root/src/providers/kimi-headers.ts +177 -0
  200. package/vendor/agent-root/src/providers/openai-compatible.ts +387 -0
  201. package/vendor/agent-root/src/providers/registry/model-config.ts +230 -0
  202. package/vendor/agent-root/src/providers/registry/provider-factory.ts +123 -0
  203. package/vendor/agent-root/src/providers/registry.ts +135 -0
  204. package/vendor/agent-root/src/providers/types/api.ts +284 -0
  205. package/vendor/agent-root/src/providers/types/config.ts +58 -0
  206. package/vendor/agent-root/src/providers/types/errors.ts +323 -0
  207. package/vendor/agent-root/src/providers/types/index.ts +72 -0
  208. package/vendor/agent-root/src/providers/types/provider.ts +45 -0
  209. package/vendor/agent-root/src/providers/types/registry.ts +88 -0
@@ -0,0 +1,1053 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import type { ToolManager } from '../../tool/tool-manager';
6
+ import { DefaultToolManager } from '../../tool/tool-manager';
7
+ import type { Chunk, LLMProvider } from '../../../providers';
8
+ import { LLMRateLimitError } from '../../../providers';
9
+ import { LLMRetryableError } from '../../../providers';
10
+ import { StatelessAgent } from '../../agent';
11
+ import { AgentAppService } from '../agent-app-service';
12
+ import { SqliteAgentAppStore } from '../sqlite-agent-app-store';
13
+ import { WriteFileTool } from '../../tool/write-file';
14
+
15
+ type ChunkDelta = NonNullable<NonNullable<Chunk['choices']>[number]>['delta'];
16
+ type TestDelta = Partial<ChunkDelta> & { finish_reason?: string };
17
+ type TestChunk = Omit<Chunk, 'choices'> & {
18
+ choices?: Array<{
19
+ index: number;
20
+ delta: TestDelta;
21
+ }>;
22
+ };
23
+
24
+ function toStream(chunks: TestChunk[]): AsyncGenerator<Chunk> {
25
+ return (async function* () {
26
+ for (const chunk of chunks) {
27
+ yield chunk as Chunk;
28
+ }
29
+ })();
30
+ }
31
+
32
+ function createProvider(): LLMProvider {
33
+ return {
34
+ config: {} as Record<string, unknown>,
35
+ generate: vi.fn(),
36
+ generateStream: vi.fn(),
37
+ getTimeTimeout: vi.fn(() => 1000),
38
+ getLLMMaxTokens: vi.fn(() => 32000),
39
+ getMaxOutputTokens: vi.fn(() => 4096),
40
+ } as unknown as LLMProvider;
41
+ }
42
+
43
+ function createToolManager(): ToolManager {
44
+ return {
45
+ execute: vi.fn(),
46
+ registerTool: vi.fn(),
47
+ getTools: vi.fn(() => []),
48
+ getConcurrencyPolicy: vi.fn(() => ({ mode: 'exclusive' as const })),
49
+ } as unknown as ToolManager;
50
+ }
51
+
52
+ async function delay(ms: number): Promise<void> {
53
+ await new Promise((resolve) => setTimeout(resolve, ms));
54
+ }
55
+
56
+ function extractJsonStringFieldPrefix(raw: string, fieldName: string): string {
57
+ const marker = `"${fieldName}":"`;
58
+ const start = raw.indexOf(marker);
59
+ if (start === -1) {
60
+ return '';
61
+ }
62
+
63
+ let cursor = start + marker.length;
64
+ let output = '';
65
+ while (cursor < raw.length) {
66
+ const ch = raw[cursor];
67
+ if (ch === '"') {
68
+ return output;
69
+ }
70
+ if (ch !== '\\') {
71
+ output += ch;
72
+ cursor += 1;
73
+ continue;
74
+ }
75
+ if (cursor + 1 >= raw.length) {
76
+ return output;
77
+ }
78
+
79
+ const esc = raw[cursor + 1];
80
+ if (esc === 'n') {
81
+ output += '\n';
82
+ cursor += 2;
83
+ continue;
84
+ }
85
+ if (esc === 'r') {
86
+ output += '\r';
87
+ cursor += 2;
88
+ continue;
89
+ }
90
+ if (esc === 't') {
91
+ output += '\t';
92
+ cursor += 2;
93
+ continue;
94
+ }
95
+ if (esc === '"' || esc === '\\' || esc === '/') {
96
+ output += esc;
97
+ cursor += 2;
98
+ continue;
99
+ }
100
+
101
+ output += esc;
102
+ cursor += 2;
103
+ }
104
+
105
+ return output;
106
+ }
107
+
108
+ describe('AgentAppService', () => {
109
+ let tempDir: string | null = null;
110
+ let store: SqliteAgentAppStore | null = null;
111
+
112
+ afterEach(async () => {
113
+ if (store) {
114
+ await store.close();
115
+ store = null;
116
+ }
117
+ if (tempDir) {
118
+ await fs.rm(tempDir, { recursive: true, force: true });
119
+ tempDir = null;
120
+ }
121
+ });
122
+
123
+ it('persists run/events and supports getRun/listRuns queries', async () => {
124
+ const provider = createProvider();
125
+ const manager = createToolManager();
126
+ provider.generateStream = vi.fn().mockReturnValue(
127
+ toStream([
128
+ {
129
+ index: 0,
130
+ choices: [{ index: 0, delta: { content: 'Hello from app service' } }],
131
+ },
132
+ {
133
+ index: 0,
134
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
135
+ },
136
+ ])
137
+ );
138
+
139
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-'));
140
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
141
+ const agent = new StatelessAgent(provider, manager, {
142
+ maxRetryCount: 2,
143
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
144
+ });
145
+ const app = new AgentAppService({
146
+ agent,
147
+ executionStore: store,
148
+ eventStore: store,
149
+ messageStore: store,
150
+ });
151
+
152
+ const result = await app.runForeground({
153
+ conversationId: 'conv_service',
154
+ executionId: 'exec_service',
155
+ userInput: 'Say hello',
156
+ maxSteps: 3,
157
+ });
158
+
159
+ expect(result.executionId).toBe('exec_service');
160
+ expect(result.finishReason).toBe('stop');
161
+ expect(result.run.status).toBe('COMPLETED');
162
+ expect(result.run.terminalReason).toBe('stop');
163
+ expect(result.events.some((event) => event.eventType === 'user_message')).toBe(true);
164
+ expect(result.events.some((event) => event.eventType === 'assistant_message')).toBe(true);
165
+ expect(result.events.some((event) => event.eventType === 'done')).toBe(true);
166
+
167
+ const run = await app.getRun('exec_service');
168
+ expect(run?.status).toBe('COMPLETED');
169
+
170
+ const list = await app.listRuns('conv_service', { limit: 10 });
171
+ expect(list.items).toHaveLength(1);
172
+ expect(list.items[0].executionId).toBe('exec_service');
173
+
174
+ const messages = await store.list('conv_service');
175
+ expect(messages.map((message) => message.role)).toEqual(['user', 'assistant']);
176
+
177
+ const contextMessages = await app.listContextMessages('conv_service');
178
+ expect(contextMessages.map((message) => message.role)).toEqual(['user', 'assistant']);
179
+
180
+ const dropped = await app.listDroppedMessages('exec_service');
181
+ expect(dropped).toHaveLength(0);
182
+
183
+ const traces = result.events.filter((event) => event.eventType === 'trace');
184
+ const metrics = result.events.filter((event) => event.eventType === 'metric');
185
+ const runLogEvents = result.events.filter((event) => event.eventType === 'run_log');
186
+ const runLogs = await app.listRunLogs('exec_service');
187
+ expect(traces.length).toBeGreaterThan(0);
188
+ expect(metrics.length).toBeGreaterThan(0);
189
+ expect(runLogEvents.length).toBeGreaterThan(0);
190
+ expect(runLogs.length).toBeGreaterThan(0);
191
+ expect(runLogs.some((log) => log.message.includes('run.start'))).toBe(true);
192
+ });
193
+
194
+ it('emits usage callback with cumulative totals and agent-calculated context usage', async () => {
195
+ const provider = createProvider();
196
+ const manager = createToolManager();
197
+ provider.generateStream = vi.fn().mockReturnValue(
198
+ toStream([
199
+ {
200
+ index: 0,
201
+ choices: [{ index: 0, delta: { content: 'usage-demo' } }],
202
+ },
203
+ {
204
+ index: 0,
205
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
206
+ },
207
+ {
208
+ index: 0,
209
+ choices: [],
210
+ usage: {
211
+ prompt_tokens: 120,
212
+ completion_tokens: 30,
213
+ total_tokens: 150,
214
+ prompt_cache_hit_tokens: 80,
215
+ prompt_cache_miss_tokens: 40,
216
+ input_tokens_details: {
217
+ cached_tokens: 80,
218
+ },
219
+ },
220
+ },
221
+ ])
222
+ );
223
+
224
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-usage-'));
225
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
226
+ const agent = new StatelessAgent(provider, manager, {
227
+ maxRetryCount: 2,
228
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
229
+ });
230
+ const app = new AgentAppService({
231
+ agent,
232
+ executionStore: store,
233
+ eventStore: store,
234
+ messageStore: store,
235
+ });
236
+
237
+ const usageEvents: Array<{
238
+ sequence: number;
239
+ stepIndex: number;
240
+ usage: {
241
+ prompt_tokens: number;
242
+ completion_tokens: number;
243
+ total_tokens: number;
244
+ prompt_cache_hit_tokens?: number;
245
+ prompt_cache_miss_tokens?: number;
246
+ input_tokens_details?: {
247
+ cached_tokens: number;
248
+ };
249
+ };
250
+ cumulativeUsage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
251
+ contextTokens?: number;
252
+ contextLimitTokens?: number;
253
+ contextUsagePercent?: number;
254
+ }> = [];
255
+
256
+ const result = await app.runForeground(
257
+ {
258
+ conversationId: 'conv_usage',
259
+ executionId: 'exec_usage',
260
+ userInput: 'Show usage',
261
+ maxSteps: 3,
262
+ },
263
+ {
264
+ onUsage: (usage) => {
265
+ usageEvents.push({
266
+ sequence: usage.sequence,
267
+ stepIndex: usage.stepIndex,
268
+ usage: usage.usage,
269
+ cumulativeUsage: usage.cumulativeUsage,
270
+ contextTokens: usage.contextTokens,
271
+ contextLimitTokens: usage.contextLimitTokens,
272
+ contextUsagePercent: usage.contextUsagePercent,
273
+ });
274
+ },
275
+ }
276
+ );
277
+
278
+ const expectedContextUsage = agent.estimateContextUsage([
279
+ {
280
+ messageId: 'msg_expected_user',
281
+ type: 'user',
282
+ role: 'user',
283
+ content: 'Show usage',
284
+ timestamp: Date.now(),
285
+ },
286
+ ]);
287
+
288
+ expect(result.finishReason).toBe('stop');
289
+ expect(usageEvents).toHaveLength(1);
290
+ expect(usageEvents[0]?.sequence).toBe(1);
291
+ expect(usageEvents[0]?.usage).toEqual({
292
+ prompt_tokens: 120,
293
+ completion_tokens: 30,
294
+ total_tokens: 150,
295
+ prompt_cache_hit_tokens: 80,
296
+ prompt_cache_miss_tokens: 40,
297
+ input_tokens_details: {
298
+ cached_tokens: 80,
299
+ },
300
+ });
301
+ expect(usageEvents[0]?.cumulativeUsage).toEqual({
302
+ prompt_tokens: 120,
303
+ completion_tokens: 30,
304
+ total_tokens: 150,
305
+ });
306
+ expect(usageEvents[0]?.contextTokens).toBe(expectedContextUsage.contextTokens);
307
+ expect(usageEvents[0]?.contextLimitTokens).toBe(expectedContextUsage.contextLimitTokens);
308
+ expect(usageEvents[0]?.contextUsagePercent).toBeCloseTo(
309
+ expectedContextUsage.contextUsagePercent,
310
+ 6
311
+ );
312
+
313
+ const storedMessages = await store.list('conv_usage');
314
+ expect(storedMessages.at(-1)?.usage).toEqual({
315
+ prompt_tokens: 120,
316
+ completion_tokens: 30,
317
+ total_tokens: 150,
318
+ prompt_cache_hit_tokens: 80,
319
+ prompt_cache_miss_tokens: 40,
320
+ input_tokens_details: {
321
+ cached_tokens: 80,
322
+ },
323
+ });
324
+ });
325
+
326
+ it('forwards onContextUsage immediately before usage events', async () => {
327
+ const provider = createProvider();
328
+ const manager = createToolManager();
329
+ provider.generateStream = vi.fn().mockReturnValue(
330
+ toStream([
331
+ {
332
+ index: 0,
333
+ choices: [{ index: 0, delta: { content: 'context-demo' } }],
334
+ usage: {
335
+ prompt_tokens: 42,
336
+ completion_tokens: 8,
337
+ total_tokens: 50,
338
+ },
339
+ },
340
+ {
341
+ index: 0,
342
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
343
+ },
344
+ ])
345
+ );
346
+
347
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-context-'));
348
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
349
+ const agent = new StatelessAgent(provider, manager, {
350
+ maxRetryCount: 2,
351
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
352
+ });
353
+ const app = new AgentAppService({
354
+ agent,
355
+ executionStore: store,
356
+ eventStore: store,
357
+ messageStore: store,
358
+ });
359
+
360
+ const callOrder: string[] = [];
361
+ const contextEvents: Array<{
362
+ stepIndex: number;
363
+ messageCount: number;
364
+ contextTokens: number;
365
+ contextLimitTokens: number;
366
+ contextUsagePercent: number;
367
+ }> = [];
368
+ const usageEvents: Array<{ stepIndex: number }> = [];
369
+
370
+ await app.runForeground(
371
+ {
372
+ conversationId: 'conv_context_usage',
373
+ executionId: 'exec_context_usage',
374
+ userInput: 'Show context',
375
+ maxSteps: 3,
376
+ },
377
+ {
378
+ onContextUsage: ((usage: {
379
+ stepIndex: number;
380
+ messageCount: number;
381
+ contextTokens: number;
382
+ contextLimitTokens: number;
383
+ contextUsagePercent: number;
384
+ }) => {
385
+ callOrder.push('context');
386
+ contextEvents.push(usage);
387
+ }) as (usage: {
388
+ stepIndex: number;
389
+ messageCount: number;
390
+ contextTokens: number;
391
+ contextLimitTokens: number;
392
+ contextUsagePercent: number;
393
+ }) => void,
394
+ onUsage: (usage) => {
395
+ callOrder.push('usage');
396
+ usageEvents.push({ stepIndex: usage.stepIndex });
397
+ },
398
+ } as Parameters<AgentAppService['runForeground']>[1]
399
+ );
400
+
401
+ expect(contextEvents).toHaveLength(1);
402
+ expect(usageEvents).toHaveLength(1);
403
+ expect(callOrder).toEqual(['context', 'usage']);
404
+ expect(contextEvents[0]?.stepIndex).toBe(1);
405
+ expect(contextEvents[0]?.messageCount).toBeGreaterThan(0);
406
+ });
407
+
408
+ it('maps aborted execution to CANCELLED terminal state', async () => {
409
+ const provider = createProvider();
410
+ const manager = createToolManager();
411
+ provider.generateStream = vi.fn().mockReturnValue(toStream([]));
412
+
413
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-abort-'));
414
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
415
+ const agent = new StatelessAgent(provider, manager, {
416
+ maxRetryCount: 1,
417
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
418
+ });
419
+ const app = new AgentAppService({
420
+ agent,
421
+ executionStore: store,
422
+ eventStore: store,
423
+ messageStore: store,
424
+ });
425
+
426
+ const controller = new AbortController();
427
+ controller.abort();
428
+
429
+ const result = await app.runForeground({
430
+ conversationId: 'conv_abort',
431
+ executionId: 'exec_abort',
432
+ userInput: 'Abort me',
433
+ abortSignal: controller.signal,
434
+ });
435
+
436
+ expect(result.finishReason).toBe('error');
437
+ expect(result.run.status).toBe('CANCELLED');
438
+ expect(result.run.terminalReason).toBe('aborted');
439
+ expect(result.run.errorCode).toBe('AGENT_ABORTED');
440
+ });
441
+
442
+ it('bridges tool chunk as tool_stream and keeps run completed', async () => {
443
+ const provider = createProvider();
444
+ const manager = createToolManager();
445
+ provider.generateStream = vi
446
+ .fn()
447
+ .mockReturnValueOnce(
448
+ toStream([
449
+ {
450
+ index: 0,
451
+ choices: [
452
+ {
453
+ index: 0,
454
+ delta: {
455
+ tool_calls: [
456
+ {
457
+ id: 'tool_call_1',
458
+ type: 'function',
459
+ index: 0,
460
+ function: {
461
+ name: 'bash',
462
+ arguments: '{"command":"echo hi"}',
463
+ },
464
+ },
465
+ ],
466
+ finish_reason: 'tool_calls',
467
+ } as unknown as ChunkDelta,
468
+ },
469
+ ],
470
+ },
471
+ ])
472
+ )
473
+ .mockReturnValueOnce(
474
+ toStream([
475
+ {
476
+ index: 0,
477
+ choices: [{ index: 0, delta: { content: 'done' } }],
478
+ },
479
+ {
480
+ index: 0,
481
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
482
+ },
483
+ ])
484
+ );
485
+
486
+ manager.execute = vi.fn().mockImplementation(async (_toolCall, options) => {
487
+ options.onChunk?.({ type: 'stdout', data: 'streamed-output' });
488
+ return { success: true, output: 'ok' };
489
+ });
490
+
491
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-tool-'));
492
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
493
+ const agent = new StatelessAgent(provider, manager, {
494
+ maxRetryCount: 2,
495
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
496
+ });
497
+ const app = new AgentAppService({
498
+ agent,
499
+ executionStore: store,
500
+ eventStore: store,
501
+ messageStore: store,
502
+ });
503
+
504
+ const result = await app.runForeground({
505
+ conversationId: 'conv_tool',
506
+ executionId: 'exec_tool',
507
+ userInput: 'run tool',
508
+ maxSteps: 3,
509
+ });
510
+
511
+ expect(result.run.status).toBe('COMPLETED');
512
+ expect(result.events.some((event) => event.eventType === 'tool_stream')).toBe(true);
513
+
514
+ const persistedEvents = await app.listRunEvents('exec_tool');
515
+ expect(persistedEvents.some((event) => event.eventType === 'tool_stream')).toBe(true);
516
+ });
517
+
518
+ it('resumes a truncated write_file direct call by recovering buffered session metadata', async () => {
519
+ const provider = createProvider();
520
+ const allowedDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-write-'));
521
+ const bufferDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-buffer-'));
522
+ const targetPath = path.join(allowedDir, 'nodejs-sandbox-implementation.md');
523
+ const directToolCallId = 'wf_resume_direct_1';
524
+
525
+ const manager = new DefaultToolManager();
526
+ manager.registerTool(
527
+ new WriteFileTool({
528
+ allowedDirectories: [allowedDir],
529
+ bufferBaseDir: bufferDir,
530
+ maxChunkBytes: 8,
531
+ })
532
+ );
533
+
534
+ const truncatedDirectArgs = JSON.stringify({
535
+ mode: 'direct',
536
+ path: targetPath,
537
+ content: '# Repro\n\n' + 'a'.repeat(64),
538
+ }).slice(0, -6);
539
+ const expectedBufferedContent = extractJsonStringFieldPrefix(truncatedDirectArgs, 'content');
540
+ const finalizeArgs = JSON.stringify({
541
+ mode: 'finalize',
542
+ bufferId: directToolCallId,
543
+ path: targetPath,
544
+ });
545
+
546
+ provider.generateStream = vi
547
+ .fn()
548
+ .mockReturnValueOnce(
549
+ toStream([
550
+ {
551
+ index: 0,
552
+ choices: [
553
+ {
554
+ index: 0,
555
+ delta: {
556
+ tool_calls: [
557
+ {
558
+ id: directToolCallId,
559
+ type: 'function',
560
+ index: 0,
561
+ function: {
562
+ name: 'write_file',
563
+ arguments: truncatedDirectArgs,
564
+ },
565
+ },
566
+ ],
567
+ finish_reason: 'tool_calls',
568
+ } as unknown as ChunkDelta,
569
+ },
570
+ ],
571
+ },
572
+ ])
573
+ )
574
+ .mockReturnValueOnce(
575
+ toStream([
576
+ {
577
+ index: 0,
578
+ choices: [
579
+ {
580
+ index: 0,
581
+ delta: {
582
+ tool_calls: [
583
+ {
584
+ id: 'wf_resume_finalize_2',
585
+ type: 'function',
586
+ index: 0,
587
+ function: {
588
+ name: 'write_file',
589
+ arguments: finalizeArgs,
590
+ },
591
+ },
592
+ ],
593
+ finish_reason: 'tool_calls',
594
+ } as unknown as ChunkDelta,
595
+ },
596
+ ],
597
+ },
598
+ ])
599
+ );
600
+
601
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-resume-'));
602
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
603
+ const agent = new StatelessAgent(provider, manager, {
604
+ maxRetryCount: 2,
605
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
606
+ });
607
+ const app = new AgentAppService({
608
+ agent,
609
+ executionStore: store,
610
+ eventStore: store,
611
+ messageStore: store,
612
+ });
613
+ try {
614
+ const firstRun = await app.runForeground({
615
+ conversationId: 'conv_resume_write_file',
616
+ executionId: 'exec_resume_write_file_1',
617
+ userInput: 'write the document',
618
+ maxSteps: 1,
619
+ });
620
+
621
+ const firstToolResult = firstRun.events.find((event) => event.eventType === 'tool_result');
622
+ expect(firstToolResult).toBeDefined();
623
+ expect(
624
+ JSON.parse((firstToolResult?.data as { content: string }).content) as {
625
+ code: string;
626
+ nextAction: string;
627
+ }
628
+ ).toMatchObject({
629
+ code: 'WRITE_FILE_PARTIAL_BUFFERED',
630
+ nextAction: 'finalize',
631
+ });
632
+
633
+ const historyMessages = await app.listContextMessages('conv_resume_write_file');
634
+ const secondRun = await app.runForeground({
635
+ conversationId: 'conv_resume_write_file',
636
+ executionId: 'exec_resume_write_file_2',
637
+ userInput: 'continue',
638
+ historyMessages,
639
+ maxSteps: 1,
640
+ });
641
+
642
+ const secondToolResult = secondRun.events.find((event) => event.eventType === 'tool_result');
643
+ expect(secondToolResult).toBeDefined();
644
+ expect(
645
+ JSON.parse((secondToolResult?.data as { content: string }).content) as {
646
+ code: string;
647
+ message: string;
648
+ nextAction: string;
649
+ }
650
+ ).toMatchObject({
651
+ code: 'WRITE_FILE_FINALIZE_OK',
652
+ nextAction: 'none',
653
+ });
654
+
655
+ expect(await fs.readFile(targetPath, 'utf8')).toBe(expectedBufferedContent);
656
+
657
+ await expect(
658
+ fs.access(path.join(bufferDir, `${directToolCallId}.pointer.json`))
659
+ ).rejects.toThrow();
660
+ expect(provider.generateStream).toHaveBeenCalledTimes(2);
661
+ } finally {
662
+ await fs.rm(allowedDir, { recursive: true, force: true });
663
+ await fs.rm(bufferDir, { recursive: true, force: true });
664
+ }
665
+ });
666
+
667
+ it('filters stored empty assistant-text history before the next llm call', async () => {
668
+ const provider = createProvider();
669
+ const manager = createToolManager();
670
+ provider.generateStream = vi
671
+ .fn()
672
+ .mockReturnValueOnce(
673
+ toStream([
674
+ {
675
+ index: 0,
676
+ choices: [{ index: 0, delta: { content: 'first answer' } }],
677
+ },
678
+ {
679
+ index: 0,
680
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
681
+ },
682
+ ])
683
+ )
684
+ .mockReturnValueOnce(
685
+ toStream([
686
+ {
687
+ index: 0,
688
+ choices: [{ index: 0, delta: { content: 'second answer' } }],
689
+ },
690
+ {
691
+ index: 0,
692
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
693
+ },
694
+ ])
695
+ );
696
+
697
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-empty-history-'));
698
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
699
+ const agent = new StatelessAgent(provider, manager, {
700
+ maxRetryCount: 3,
701
+ toolExecutionLedger: undefined,
702
+ });
703
+ const app = new AgentAppService({
704
+ agent,
705
+ executionStore: store,
706
+ eventStore: store,
707
+ messageStore: store,
708
+ });
709
+
710
+ const firstRun = await app.runForeground({
711
+ conversationId: 'conv_empty_history_filter',
712
+ executionId: 'exec_empty_history_filter_1',
713
+ userInput: 'first turn',
714
+ });
715
+
716
+ const storedMessages = [...firstRun.messages];
717
+ storedMessages.push({
718
+ messageId: 'msg_empty_assistant',
719
+ role: 'assistant',
720
+ type: 'assistant-text',
721
+ content: '',
722
+ reasoning_content: '',
723
+ timestamp: Date.now(),
724
+ });
725
+
726
+ await app.runForeground({
727
+ conversationId: 'conv_empty_history_filter',
728
+ executionId: 'exec_empty_history_filter_2',
729
+ userInput: 'second turn',
730
+ historyMessages: storedMessages,
731
+ });
732
+
733
+ const generateStreamCalls = (
734
+ provider.generateStream as unknown as { mock: { calls: unknown[][] } }
735
+ ).mock.calls;
736
+ expect(generateStreamCalls).toHaveLength(2);
737
+ const secondCallMessages = generateStreamCalls[1]?.[0] as Array<{
738
+ role: string;
739
+ content: unknown;
740
+ }>;
741
+ expect(
742
+ secondCallMessages.some(
743
+ (message) =>
744
+ message.role === 'assistant' &&
745
+ typeof message.content === 'string' &&
746
+ message.content === ''
747
+ )
748
+ ).toBe(false);
749
+ expect(
750
+ secondCallMessages.some(
751
+ (message) => message.role === 'assistant' && message.content === 'first answer'
752
+ )
753
+ ).toBe(true);
754
+ });
755
+
756
+ it('maps AGENT_UPSTREAM_TIMEOUT to FAILED timeout terminal state', async () => {
757
+ const provider = createProvider();
758
+ const manager = createToolManager();
759
+ provider.generateStream = vi.fn().mockImplementation(() =>
760
+ (async function* () {
761
+ for (const chunk of [] as Chunk[]) {
762
+ yield chunk;
763
+ }
764
+ throw new LLMRetryableError('Request timeout', undefined, 'TIMEOUT');
765
+ })()
766
+ );
767
+
768
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-upstream-timeout-'));
769
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
770
+ const agent = new StatelessAgent(provider, manager, {
771
+ maxRetryCount: 3,
772
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
773
+ });
774
+ const app = new AgentAppService({
775
+ agent,
776
+ executionStore: store,
777
+ eventStore: store,
778
+ messageStore: store,
779
+ });
780
+
781
+ const result = await app.runForeground(
782
+ {
783
+ conversationId: 'conv_upstream_timeout',
784
+ executionId: 'exec_upstream_timeout',
785
+ userInput: 'trigger timeout',
786
+ },
787
+ {
788
+ onError: async () => ({ retry: false }),
789
+ }
790
+ );
791
+
792
+ expect(result.finishReason).toBe('error');
793
+ expect(result.run.status).toBe('FAILED');
794
+ expect(result.run.terminalReason).toBe('timeout');
795
+ expect(result.run.errorCode).toBe('AGENT_UPSTREAM_TIMEOUT');
796
+ });
797
+
798
+ it('maps AGENT_UPSTREAM_RATE_LIMIT to FAILED rate_limit terminal state', async () => {
799
+ const provider = createProvider();
800
+ const manager = createToolManager();
801
+ provider.generateStream = vi.fn().mockImplementation(() =>
802
+ (async function* () {
803
+ for (const chunk of [] as Chunk[]) {
804
+ yield chunk;
805
+ }
806
+ throw new LLMRateLimitError('Too many requests');
807
+ })()
808
+ );
809
+
810
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-rate-limit-'));
811
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
812
+ const agent = new StatelessAgent(provider, manager, {
813
+ maxRetryCount: 3,
814
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
815
+ });
816
+ const app = new AgentAppService({
817
+ agent,
818
+ executionStore: store,
819
+ eventStore: store,
820
+ messageStore: store,
821
+ });
822
+
823
+ const result = await app.runForeground(
824
+ {
825
+ conversationId: 'conv_rate_limit',
826
+ executionId: 'exec_rate_limit',
827
+ userInput: 'trigger rate limit',
828
+ },
829
+ {
830
+ onError: async () => ({ retry: false }),
831
+ }
832
+ );
833
+
834
+ expect(result.finishReason).toBe('error');
835
+ expect(result.run.status).toBe('FAILED');
836
+ expect(result.run.terminalReason).toBe('rate_limit');
837
+ expect(result.run.errorCode).toBe('AGENT_UPSTREAM_RATE_LIMIT');
838
+ });
839
+
840
+ it('keeps upstream server/network/generic retryable terminal reason as error', async () => {
841
+ const provider = createProvider();
842
+ const manager = createToolManager();
843
+ provider.generateStream = vi
844
+ .fn()
845
+ .mockImplementationOnce(() =>
846
+ (async function* () {
847
+ for (const chunk of [] as Chunk[]) {
848
+ yield chunk;
849
+ }
850
+ throw new LLMRetryableError('Server unavailable', undefined, 'SERVER_503');
851
+ })()
852
+ )
853
+ .mockImplementationOnce(() =>
854
+ (async function* () {
855
+ for (const chunk of [] as Chunk[]) {
856
+ yield chunk;
857
+ }
858
+ throw new LLMRetryableError('Network unstable', undefined, 'NETWORK_ERROR');
859
+ })()
860
+ )
861
+ .mockImplementationOnce(() =>
862
+ (async function* () {
863
+ for (const chunk of [] as Chunk[]) {
864
+ yield chunk;
865
+ }
866
+ throw new LLMRetryableError('Retry later', undefined, 'TRANSIENT_X');
867
+ })()
868
+ );
869
+
870
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-upstream-error-'));
871
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
872
+ const agent = new StatelessAgent(provider, manager, {
873
+ maxRetryCount: 3,
874
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
875
+ });
876
+ const app = new AgentAppService({
877
+ agent,
878
+ executionStore: store,
879
+ eventStore: store,
880
+ messageStore: store,
881
+ });
882
+
883
+ const serverResult = await app.runForeground(
884
+ {
885
+ conversationId: 'conv_upstream_error',
886
+ executionId: 'exec_upstream_server',
887
+ userInput: 'trigger upstream server',
888
+ },
889
+ {
890
+ onError: async () => ({ retry: false }),
891
+ }
892
+ );
893
+ const networkResult = await app.runForeground(
894
+ {
895
+ conversationId: 'conv_upstream_error',
896
+ executionId: 'exec_upstream_network',
897
+ userInput: 'trigger upstream network',
898
+ },
899
+ {
900
+ onError: async () => ({ retry: false }),
901
+ }
902
+ );
903
+ const genericRetryableResult = await app.runForeground(
904
+ {
905
+ conversationId: 'conv_upstream_error',
906
+ executionId: 'exec_upstream_retryable',
907
+ userInput: 'trigger upstream retryable',
908
+ },
909
+ {
910
+ onError: async () => ({ retry: false }),
911
+ }
912
+ );
913
+
914
+ expect(serverResult.run.status).toBe('FAILED');
915
+ expect(serverResult.run.terminalReason).toBe('error');
916
+ expect(serverResult.run.errorCode).toBe('AGENT_UPSTREAM_SERVER');
917
+
918
+ expect(networkResult.run.status).toBe('FAILED');
919
+ expect(networkResult.run.terminalReason).toBe('error');
920
+ expect(networkResult.run.errorCode).toBe('AGENT_UPSTREAM_NETWORK');
921
+
922
+ expect(genericRetryableResult.run.status).toBe('FAILED');
923
+ expect(genericRetryableResult.run.terminalReason).toBe('error');
924
+ expect(genericRetryableResult.run.errorCode).toBe('AGENT_UPSTREAM_RETRYABLE');
925
+ });
926
+
927
+ it('isolates run logs across concurrent executions on the same service instance', async () => {
928
+ const provider = createProvider();
929
+ const manager = createToolManager();
930
+ provider.generateStream = vi.fn().mockImplementation((messages: Array<{ content: string }>) => {
931
+ const marker = messages.at(-1)?.content ?? 'unknown';
932
+ return (async function* () {
933
+ await delay(marker === 'first' ? 10 : 1);
934
+ yield {
935
+ index: 0,
936
+ choices: [{ index: 0, delta: { content: `reply:${marker}` } }],
937
+ } as Chunk;
938
+ await delay(marker === 'first' ? 1 : 10);
939
+ yield {
940
+ index: 0,
941
+ choices: [{ index: 0, delta: { finish_reason: 'stop' } as unknown as ChunkDelta }],
942
+ } as Chunk;
943
+ })();
944
+ });
945
+
946
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-concurrent-'));
947
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
948
+ const agent = new StatelessAgent(provider, manager, {
949
+ maxRetryCount: 2,
950
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
951
+ });
952
+ const app = new AgentAppService({
953
+ agent,
954
+ executionStore: store,
955
+ eventStore: store,
956
+ messageStore: store,
957
+ });
958
+
959
+ const [first, second] = await Promise.all([
960
+ app.runForeground({
961
+ conversationId: 'conv_concurrent',
962
+ executionId: 'exec_first',
963
+ userInput: 'first',
964
+ maxSteps: 3,
965
+ }),
966
+ app.runForeground({
967
+ conversationId: 'conv_concurrent',
968
+ executionId: 'exec_second',
969
+ userInput: 'second',
970
+ maxSteps: 3,
971
+ }),
972
+ ]);
973
+
974
+ expect(first.run.status).toBe('COMPLETED');
975
+ expect(second.run.status).toBe('COMPLETED');
976
+
977
+ const firstLogs = await app.listRunLogs('exec_first');
978
+ const secondLogs = await app.listRunLogs('exec_second');
979
+ expect(firstLogs.length).toBeGreaterThan(0);
980
+ expect(secondLogs.length).toBeGreaterThan(0);
981
+ expect(firstLogs.every((log) => log.executionId === 'exec_first')).toBe(true);
982
+ expect(secondLogs.every((log) => log.executionId === 'exec_second')).toBe(true);
983
+ expect(
984
+ firstLogs.every(
985
+ (log) => log.context?.executionId === undefined || log.context.executionId === 'exec_first'
986
+ )
987
+ ).toBe(true);
988
+ expect(
989
+ secondLogs.every(
990
+ (log) => log.context?.executionId === undefined || log.context.executionId === 'exec_second'
991
+ )
992
+ ).toBe(true);
993
+ });
994
+
995
+ it('persists error run logs with structured details when execution fails', async () => {
996
+ const provider = createProvider();
997
+ const manager = createToolManager();
998
+ provider.generateStream = vi.fn().mockImplementation(() =>
999
+ (async function* () {
1000
+ for (const chunk of [] as Chunk[]) {
1001
+ yield chunk;
1002
+ }
1003
+ throw new Error('provider exploded');
1004
+ })()
1005
+ );
1006
+
1007
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-app-service-error-logs-'));
1008
+ store = new SqliteAgentAppStore(path.join(tempDir, 'agent.db'));
1009
+ const agent = new StatelessAgent(provider, manager, {
1010
+ maxRetryCount: 1,
1011
+ backoffConfig: { initialDelayMs: 1, maxDelayMs: 1, base: 2, jitter: false },
1012
+ });
1013
+ const app = new AgentAppService({
1014
+ agent,
1015
+ executionStore: store,
1016
+ eventStore: store,
1017
+ messageStore: store,
1018
+ });
1019
+
1020
+ const result = await app.runForeground(
1021
+ {
1022
+ conversationId: 'conv_error_logs',
1023
+ executionId: 'exec_error_logs',
1024
+ userInput: 'explode',
1025
+ },
1026
+ {
1027
+ onError: async () => ({ retry: false }),
1028
+ }
1029
+ );
1030
+
1031
+ expect(result.run.status).toBe('FAILED');
1032
+ const errorLogs = await app.listRunLogs('exec_error_logs', { level: 'error' });
1033
+ expect(errorLogs.length).toBeGreaterThan(0);
1034
+ expect(errorLogs.some((log) => log.message === '[Agent] run.error')).toBe(true);
1035
+ expect(errorLogs).toContainEqual(
1036
+ expect.objectContaining({
1037
+ executionId: 'exec_error_logs',
1038
+ level: 'error',
1039
+ message: '[Agent] run.error',
1040
+ error: expect.objectContaining({
1041
+ message: 'provider exploded',
1042
+ }),
1043
+ })
1044
+ );
1045
+
1046
+ const runLogEvents = result.events.filter((event) => event.eventType === 'run_log');
1047
+ expect(
1048
+ runLogEvents.some(
1049
+ (event) => (event.data as { message?: string }).message === '[Agent] run.error'
1050
+ )
1051
+ ).toBe(true);
1052
+ });
1053
+ });