agent-harness 0.5.9 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f476f6224ed25cc79c7bc9c8fb239c1df9f787cd59ff279b8c509b7d515d2727
4
- data.tar.gz: c0faee9c6d5c139db019707b9174d2d25a4a2fc4a85684ce55eb7baede9fd3be
3
+ metadata.gz: 0a5be3b23b73351341808a0f62fb6e060f226c309ceb508c378ad3d549273e61
4
+ data.tar.gz: aa6f754664cd08deeb36f8a54ec3531249a10c80bbe6561706a59db1191afe4f
5
5
  SHA512:
6
- metadata.gz: 113c0d8afbf5e77c6abcb1f78f0793646b72137e48a9776bfd951ee98c5412d3606628ea19ce4b105a6d8c05d1143a12a5607e1bad82462c629f6bdf3907985b
7
- data.tar.gz: 4cc2f012c75314a2d096147e4bae9afbed565181878a907339410f7cfdb3ea0cff03e96704a6d3d93cd4f3825eb9f21a97d5351b852d72ec4e09b21a3359df68
6
+ metadata.gz: d28ce3a2ecc67df9f125c1bb82f51192811fd5cd8394252f9450ec746876d125d2487a65eef6bbf925bdb4438bd74e554213d9ac5aacbbca398357ba32ae1570
7
+ data.tar.gz: 63f514add56b1975e1d1a70f1962f8512a9af6eb4427ce893d4fd1c364bc054824fd5208198d61a6ecab0162cd4d382351bb09bcec7c4c0c7f63c5804ac007cd
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.9"
2
+ ".": "0.7.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,165 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.6.0...agent-harness/v0.7.0) (2026-04-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * **copilot:** add JSON output parsing and token extraction ([4f5fc5a](https://github.com/viamin/agent-harness/commit/4f5fc5acd8d45ac8563998a132a0c4878f3b9e0a))
9
+ * **kilocode:** extract token usage from Kilo CLI structured JSON output ([b5384f8](https://github.com/viamin/agent-harness/commit/b5384f8be52431f95d8aa3524a33ceed6bf094eb)), closes [#97](https://github.com/viamin/agent-harness/issues/97)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **copilot:** add nil guard for stdout and improve error string construction ([6a30ce3](https://github.com/viamin/agent-harness/commit/6a30ce342100b27c0b16fc8c2abdce48bbf10ef7))
15
+ * **copilot:** align error ordering with base parser ([0a02d34](https://github.com/viamin/agent-harness/commit/0a02d34cbf07e4f4ecb4d3efc6d69b5b072c6114))
16
+ * **copilot:** align metadata and reply parsing ([e5c3387](https://github.com/viamin/agent-harness/commit/e5c338743dbb5ec8eea5b1a8de5a515f1df7e141))
17
+ * **copilot:** avoid double-counting token aliases ([40e78f3](https://github.com/viamin/agent-harness/commit/40e78f34a6304a5ae21e26c6c291eed773618bea))
18
+ * **copilot:** avoid mixing shutdown token totals ([c4bdfb8](https://github.com/viamin/agent-harness/commit/c4bdfb8fd4c20781ef4621cf421947b14514cb45))
19
+ * **copilot:** drop superseded delta chunks ([769acd6](https://github.com/viamin/agent-harness/commit/769acd6a45f1037f11fc83ea23f5ddeda9aadd17))
20
+ * **copilot:** fall back across malformed token aliases ([9c9f5f8](https://github.com/viamin/agent-harness/commit/9c9f5f8048f63407b6ffc14fb339c26158f74dab))
21
+ * **copilot:** fall back from empty nested message content ([ecd9f49](https://github.com/viamin/agent-harness/commit/ecd9f497e0ef9812f2df363b02679b5842cf668c))
22
+ * **copilot:** fall back from empty shutdown metrics ([0397f1e](https://github.com/viamin/agent-harness/commit/0397f1e5f9442d0d6489c9fe7744d31a0ff48965))
23
+ * **copilot:** fall back from malformed nested message content ([a313487](https://github.com/viamin/agent-harness/commit/a313487f2c1bb9fbf5e7a60e5dc08e7a7079447c))
24
+ * **copilot:** fall back from malformed usage payloads ([733599c](https://github.com/viamin/agent-harness/commit/733599c599cda90873cec823c245b13c81f74ee6))
25
+ * **copilot:** gate json output by cli version ([528d03b](https://github.com/viamin/agent-harness/commit/528d03bed7506996cf9cfd6c4cf54807da254260))
26
+ * **copilot:** guard scalar json events ([13a4131](https://github.com/viamin/agent-harness/commit/13a413157cc159cfc4fd6b7e7ab7fdbb948d07b6))
27
+ * **copilot:** handle JSON event envelopes and camelCase token fields ([e0ee83e](https://github.com/viamin/agent-harness/commit/e0ee83ed73d715d9c67806b5253abab33cce9e19))
28
+ * **copilot:** hash unresolved probe path keys ([ea9aca2](https://github.com/viamin/agent-harness/commit/ea9aca215b000833be18bf035cba6c0bf029615d))
29
+ * **copilot:** hide structured control events from output ([81c108d](https://github.com/viamin/agent-harness/commit/81c108d521e8f72522dc1dc604cccdf46f0d01d4))
30
+ * **copilot:** ignore delta chunks after final reply ([6aef1ca](https://github.com/viamin/agent-harness/commit/6aef1ca4459a5588fc53b70692f95c6eff3b9d88))
31
+ * **copilot:** ignore empty delta chunks ([dca6395](https://github.com/viamin/agent-harness/commit/dca63951866a4dc65b2adca3b834d05a6716f298))
32
+ * **copilot:** ignore failed version probes ([fa6ba35](https://github.com/viamin/agent-harness/commit/fa6ba35e1e36f153f47bccf9c423cea7927176e7))
33
+ * **copilot:** ignore malformed delta content fallback ([0c9211b](https://github.com/viamin/agent-harness/commit/0c9211bb16e713f4b7185c1b9ccda5d065b9d405))
34
+ * **copilot:** ignore malformed token payloads ([0f2f06b](https://github.com/viamin/agent-harness/commit/0f2f06b9a8c0b4244c7b8040e5590cc6e4710143))
35
+ * **copilot:** ignore malformed typed json fallbacks ([0cd5535](https://github.com/viamin/agent-harness/commit/0cd553594aff277ce07abd3d151028ad22b3f597))
36
+ * **copilot:** ignore nested non-assistant fallback text ([799f976](https://github.com/viamin/agent-harness/commit/799f976ef19618acaae1a6e0ecf7faa76b9ff37f))
37
+ * **copilot:** ignore non-assistant top-level messages ([119c854](https://github.com/viamin/agent-harness/commit/119c8540e7db9f1827eac75e73403b638cf8db23))
38
+ * **copilot:** ignore non-assistant top-level token payloads ([23c05b9](https://github.com/viamin/agent-harness/commit/23c05b9dbc6231755783f4a8eefd5099067f9389))
39
+ * **copilot:** ignore partial invalid token aliases ([3344c07](https://github.com/viamin/agent-harness/commit/3344c078d291277b8831ab69fa5d2a40ca95135b))
40
+ * **copilot:** isolate probe cache for PATH overrides ([5fa79a9](https://github.com/viamin/agent-harness/commit/5fa79a91fd90dd33a15d5c6a0c25c2619b8f0ac3))
41
+ * **copilot:** keep delta output on empty final reply ([64fbf68](https://github.com/viamin/agent-harness/commit/64fbf68ffb66b58d7546bc1841c78fec86201acb))
42
+ * **copilot:** keep preflight errors inside base handler ([49c9075](https://github.com/viamin/agent-harness/commit/49c9075d4a1fb9ff42d633723cb5f375e8c2721c))
43
+ * **copilot:** merge partial shutdown token totals ([49d6b2b](https://github.com/viamin/agent-harness/commit/49d6b2b6727297a58e9aa265347c781405012aa0))
44
+ * **copilot:** merge top-level token fallbacks ([826c1f9](https://github.com/viamin/agent-harness/commit/826c1f9fd427b10c40fb34c5b47f4c9fb06f1e64))
45
+ * **copilot:** parse session shutdown token totals ([0148afd](https://github.com/viamin/agent-harness/commit/0148afd2f95312eae687799541545dd8ece185f3))
46
+ * **copilot:** parse streamed delta reply events ([55f553f](https://github.com/viamin/agent-harness/commit/55f553f39dbe729588641b86168f8c36500ec872))
47
+ * **copilot:** prefer final replies and trim probe cache keys ([38dc20e](https://github.com/viamin/agent-harness/commit/38dc20edac2326265c8f9d149394650b70c43e90))
48
+ * **copilot:** prefer final reply over delta chunks ([955654d](https://github.com/viamin/agent-harness/commit/955654db325cc47c8cf0ac992de55e574a45c641))
49
+ * **copilot:** prefer nested assistant message fallback ([15aa6db](https://github.com/viamin/agent-harness/commit/15aa6dbf4bb171eaaa5b85c552008953141ba622))
50
+ * **copilot:** prefer per-turn usage over shutdown totals ([38bd47c](https://github.com/viamin/agent-harness/commit/38bd47c00e5d74ef365a74dc61df9e2fdc5b9c04))
51
+ * **copilot:** prefer populated top-level usage payloads ([8b3aac0](https://github.com/viamin/agent-harness/commit/8b3aac029004f65efe26e8b3fc27f62ac2008dca))
52
+ * **copilot:** preserve blank mixed output lines ([e9830fc](https://github.com/viamin/agent-harness/commit/e9830fcfcceeb7fc67caac658ed16867e1f303d0))
53
+ * **copilot:** preserve empty nested message payloads ([edf27bc](https://github.com/viamin/agent-harness/commit/edf27bcafbfb29dc0f5baf54fcedb8e959e20bba))
54
+ * **copilot:** preserve empty top-level fallback payloads ([1863854](https://github.com/viamin/agent-harness/commit/1863854af8545573a6c9a80cc47297487ee6d2ac))
55
+ * **copilot:** preserve legitimate exit codes in responses ([d1a3cc0](https://github.com/viamin/agent-harness/commit/d1a3cc0f21ea1a45fee0dbf181462efc8b414fc8))
56
+ * **copilot:** preserve literal json stdout ([7d7862c](https://github.com/viamin/agent-harness/commit/7d7862c4da83ccf9bafbbca19b595911923edeed))
57
+ * **copilot:** preserve malformed top-level json output ([f7c5bec](https://github.com/viamin/agent-harness/commit/f7c5becaa01584c53f1327be0afbd2fcebe7f3e8))
58
+ * **copilot:** preserve malformed usage hashes ([0eef69b](https://github.com/viamin/agent-harness/commit/0eef69b470927412c405a774125ca0aa9a58e302))
59
+ * **copilot:** preserve mixed json and text output ([95de0f7](https://github.com/viamin/agent-harness/commit/95de0f72a2bd8a86d33ab0806fb1baf481e52d03))
60
+ * **copilot:** preserve mixed output line boundaries ([b285526](https://github.com/viamin/agent-harness/commit/b28552627192aa613c73c7bf2c8c8afed6811c36))
61
+ * **copilot:** preserve mixed plain-text output ([d277f3a](https://github.com/viamin/agent-harness/commit/d277f3a4ed2799182a2083989a0bdaf9960df16b))
62
+ * **copilot:** preserve non-event typed json output ([27d01c6](https://github.com/viamin/agent-harness/commit/27d01c649cab65ba2da206636f45170af5abbcc9))
63
+ * **copilot:** preserve scalar json stdout ([7c5e74c](https://github.com/viamin/agent-harness/commit/7c5e74cd9aed9de17f572b98faac9c09b8cd9707))
64
+ * **copilot:** preserve unknown typed json output ([942edac](https://github.com/viamin/agent-harness/commit/942edac7bc36d9e26a7c02945f15503ce7faeb73))
65
+ * **copilot:** preserve zero token aliases ([853d251](https://github.com/viamin/agent-harness/commit/853d251869adb3cc36bebc5b6d750a58c70843f7))
66
+ * **copilot:** probe json support per request env ([da94082](https://github.com/viamin/agent-harness/commit/da94082a79a044eb89b470b26eeda29196e7ac95))
67
+ * **copilot:** reject invalid token counts ([106c386](https://github.com/viamin/agent-harness/commit/106c3867d024751e77aa0fc215c651de87230a13))
68
+ * **copilot:** restore reply token fallback ([c9d7182](https://github.com/viamin/agent-harness/commit/c9d71820b0becc80e44a77169c2990be20469be2))
69
+ * **copilot:** restrict token accumulation to usage event types only ([80c979b](https://github.com/viamin/agent-harness/commit/80c979b9952defa05923920a737bb74e1c8885e4))
70
+ * **copilot:** skip blank assistant boundaries ([b9dec9a](https://github.com/viamin/agent-harness/commit/b9dec9af0c6084e1809dc1fb1c50535b987037a1))
71
+ * **copilot:** skip json parsing in legacy mode ([af9ac12](https://github.com/viamin/agent-harness/commit/af9ac1253aa4246c92727bc04f23d6704ec8926e))
72
+ * **copilot:** store probe env per thread ([301f2aa](https://github.com/viamin/agent-harness/commit/301f2aa4c43b183dea8450d21eb7fec799e92ec6))
73
+ * **copilot:** stub json support in parser specs ([96a945e](https://github.com/viamin/agent-harness/commit/96a945ec1572e10231b696008c7514d1acb92c11))
74
+ * **copilot:** sum reply token fallback ([82c5097](https://github.com/viamin/agent-harness/commit/82c5097b73f17e64cd3015c34aa92bfd713c0428))
75
+ * **copilot:** support snake_case delta chunks ([d33ecbc](https://github.com/viamin/agent-harness/commit/d33ecbcc446d895d35f03a7513ad7b16f0f57b1d))
76
+ * **copilot:** support snake_case shutdown metrics ([8c6e93e](https://github.com/viamin/agent-harness/commit/8c6e93e70e0927c95c63811b247291faaceacab0))
77
+ * **copilot:** suppress additional control events ([b14b8bb](https://github.com/viamin/agent-harness/commit/b14b8bbebb2fcc0c975788c1168965bee3281a79))
78
+ * **copilot:** suppress control event namespaces ([7b31bb2](https://github.com/viamin/agent-harness/commit/7b31bb293fb858808e493a23e268d89a590741a0))
79
+ * **copilot:** suppress root control events ([5d4d3ec](https://github.com/viamin/agent-harness/commit/5d4d3ecb80864ad4d20e0da1083ac02d90773fbf))
80
+ * **copilot:** update output_format metadata and add missing parse_response tests ([7fdedde](https://github.com/viamin/agent-harness/commit/7fdeddee3bbe5eca96f29f5613a5c36189695ec4))
81
+ * **kilocode:** aggregate token counts across multiple step_finish events ([23c8c55](https://github.com/viamin/agent-harness/commit/23c8c55def0452de1e0b25765c4f6d1fcd3474d4))
82
+ * **kilocode:** avoid raw ndjson in structured failures ([932e56e](https://github.com/viamin/agent-harness/commit/932e56e06567095750a7bcf15bf5adea065eab44))
83
+ * **kilocode:** clear stale extra usage categories ([2ff7079](https://github.com/viamin/agent-harness/commit/2ff70795a9e3397d5844de92c47a59b49a599426))
84
+ * **kilocode:** count extra result usage tokens ([f65dd3d](https://github.com/viamin/agent-harness/commit/f65dd3d8e4f54174b18233ddd55dba2d3c3fd1f3))
85
+ * **kilocode:** count reasoning and cache step tokens ([c46d089](https://github.com/viamin/agent-harness/commit/c46d089d4d0492534d98736dcdcb1ff82a67d0c1))
86
+ * **kilocode:** fail on structured error events ([48f5378](https://github.com/viamin/agent-harness/commit/48f5378f37cf64af28238c67314a01a3758aab05))
87
+ * **kilocode:** fall back to result text after blank chunks ([7508c58](https://github.com/viamin/agent-harness/commit/7508c58a84f365a680578891dd582a6f13484f1e))
88
+ * **kilocode:** fall back to step token totals when usage is incomplete ([8534691](https://github.com/viamin/agent-harness/commit/8534691ff74ba4efa1ae5d50de329bc53e863bcf))
89
+ * **kilocode:** fall through blank part message aliases ([85db058](https://github.com/viamin/agent-harness/commit/85db05824684cde93755c4d87323bce79e0a1dd7))
90
+ * **kilocode:** fall through blank part text chunks ([417a4c8](https://github.com/viamin/agent-harness/commit/417a4c8005f23c28f5a68a834890614ca49c5dca))
91
+ * **kilocode:** fall through blank text aliases ([63ed6a1](https://github.com/viamin/agent-harness/commit/63ed6a1bb002329ab823a8844e0ac81ef065938b))
92
+ * **kilocode:** guard malformed structured output payloads ([30a59a2](https://github.com/viamin/agent-harness/commit/30a59a21d3b4012cec4dbfa27b5471b9dee329bd))
93
+ * **kilocode:** guard scalar structured error payloads ([26843a9](https://github.com/viamin/agent-harness/commit/26843a90019d3f29f15eb363d098f0bba0f00582))
94
+ * **kilocode:** guard step_finish part.tokens against non-Hash values ([ff2d841](https://github.com/viamin/agent-harness/commit/ff2d8414eb928bd679d9754a71d0849963298a4b))
95
+ * **kilocode:** honor explicit usage totals ([d1e5275](https://github.com/viamin/agent-harness/commit/d1e5275f3515ae40db4b523e0efe2ce8a3d169a1))
96
+ * **kilocode:** honor later explicit total alias updates ([496df42](https://github.com/viamin/agent-harness/commit/496df425a8a5e76e97134dea8b1f74a0067aad1b))
97
+ * **kilocode:** honor synthesized result usage totals ([b0779a7](https://github.com/viamin/agent-harness/commit/b0779a78400c1a3b6aa38610582c5cf96446b28e))
98
+ * **kilocode:** honor valid total fallback aliases ([d0ae122](https://github.com/viamin/agent-harness/commit/d0ae12245be3e606d0919bf78dcc858525c64036))
99
+ * **kilocode:** ignore blank terminal result strings ([ebb34b6](https://github.com/viamin/agent-harness/commit/ebb34b663c7c0a933aedc17efba444263a891b80))
100
+ * **kilocode:** ignore negative token counts ([aee8d1e](https://github.com/viamin/agent-harness/commit/aee8d1e35e438ae25bf3a37571d273d891b89b24))
101
+ * **kilocode:** ignore non-string text payloads ([4a35a6f](https://github.com/viamin/agent-harness/commit/4a35a6fb1555923b3d1555bc895051bbaa7db26c))
102
+ * **kilocode:** ignore scalar json fallback noise ([1f62463](https://github.com/viamin/agent-harness/commit/1f624631477557ae533d605ecf37a5fb40c39544))
103
+ * **kilocode:** ignore usage on non-usage events ([b23ac10](https://github.com/viamin/agent-harness/commit/b23ac10c0d4107e05a50a2dc83204c0088bddbbb))
104
+ * **kilocode:** ignore whitespace-only text alias placeholders ([62bb641](https://github.com/viamin/agent-harness/commit/62bb641a7b7ac5434b2e905c6b297b2abf989020))
105
+ * **kilocode:** ignore whitespace-only text chunks ([699cccc](https://github.com/viamin/agent-harness/commit/699cccc1025b83a3850b7ed2670ff60de39e6adf))
106
+ * **kilocode:** keep last usable structured usage payload ([1b9d9bd](https://github.com/viamin/agent-harness/commit/1b9d9bd7ebbaa0ae9ed6dd7379f9b8c67de19025))
107
+ * **kilocode:** keep stdout diagnostics with structured errors ([76446fc](https://github.com/viamin/agent-harness/commit/76446fc8a95c89c9944b47db794ddb776e29686f))
108
+ * **kilocode:** merge partial structured usage events ([2de37e2](https://github.com/viamin/agent-harness/commit/2de37e246317650f60a0399c2f731f03ecf28ec9))
109
+ * **kilocode:** normalize malformed token counts ([ef4c3dc](https://github.com/viamin/agent-harness/commit/ef4c3dce783e71aae1b60c543da59f64b4efdb6c))
110
+ * **kilocode:** parse hash-shaped structured error aliases ([431f8c4](https://github.com/viamin/agent-harness/commit/431f8c438d7b13bf45d3416e947d3ad34b8b4eca))
111
+ * **kilocode:** parse NDJSON event stream instead of single JSON object ([4e1252f](https://github.com/viamin/agent-harness/commit/4e1252fc384c51d118b43647a7026281927b6794))
112
+ * **kilocode:** parse nested part error messages ([8d49794](https://github.com/viamin/agent-harness/commit/8d49794f79bbe553cfe8f569867508bb80a86805))
113
+ * **kilocode:** parse nested structured error messages ([b9fee6a](https://github.com/viamin/agent-harness/commit/b9fee6a8814045f0c5329bac23b0f0b424b17566))
114
+ * **kilocode:** parse token usage from step_finish.part.tokens ([bbf5a58](https://github.com/viamin/agent-harness/commit/bbf5a58fc6bc631a483a3e71bc0d8b99744e9f7b))
115
+ * **kilocode:** pass legitimate_exit_codes in Response metadata ([dac77c2](https://github.com/viamin/agent-harness/commit/dac77c24711bf717b7d98d847a21d3922889026b))
116
+ * **kilocode:** preserve base error stream ordering ([5281d78](https://github.com/viamin/agent-harness/commit/5281d7875c54cd010364b251069d921c4c5e4838))
117
+ * **kilocode:** preserve extra usage totals without io counts ([b4fb265](https://github.com/viamin/agent-harness/commit/b4fb2657aa56ad42e514a42dd0176742a48fdbc4))
118
+ * **kilocode:** preserve json array fallback output ([16cb566](https://github.com/viamin/agent-harness/commit/16cb5668e1442a9232ca5fcc38d0b196eb673956))
119
+ * **kilocode:** preserve mixed structured failure diagnostics ([5ec3be7](https://github.com/viamin/agent-harness/commit/5ec3be7ff143803c8e9602a1d76b6c7c4c0beb12))
120
+ * **kilocode:** preserve mixed structured success output ([ea64e2e](https://github.com/viamin/agent-harness/commit/ea64e2ea7154c15193ce35c115874bb898dcdc84))
121
+ * **kilocode:** preserve provider token totals ([b62d749](https://github.com/viamin/agent-harness/commit/b62d7499070aa5233c56b7207b6a2aede97a7dfc))
122
+ * **kilocode:** preserve raw mixed stdout spacing ([881c51e](https://github.com/viamin/agent-harness/commit/881c51e9a88705e0d8e28e06cb7fd7381f55feed))
123
+ * **kilocode:** preserve raw output for non-event json ([ce9be0f](https://github.com/viamin/agent-harness/commit/ce9be0f5f8d807dda995202fbe2dc26cddee32b2))
124
+ * **kilocode:** preserve step extras with partial usage ([14eeddb](https://github.com/viamin/agent-harness/commit/14eeddb6ec9e056c8b20ee9251e42a26de260079))
125
+ * **kilocode:** preserve step token totals for partial usage ([5efe2f4](https://github.com/viamin/agent-harness/commit/5efe2f4d3804f52cfae1b6ee63da87d3459a8c0f))
126
+ * **kilocode:** preserve terminal result payload spacing ([977faa7](https://github.com/viamin/agent-harness/commit/977faa7eed454c6070503589cc995f3021d19476))
127
+ * **kilocode:** preserve terminal result text ([282ae35](https://github.com/viamin/agent-harness/commit/282ae35051b715f0210534f31adb87152c75b6b7))
128
+ * **kilocode:** preserve terminal result text across result events ([5652453](https://github.com/viamin/agent-harness/commit/56524531434d9d7c71f7af3acd17d25dbf9656ca))
129
+ * **kilocode:** preserve unreconstructable step totals ([a906ee9](https://github.com/viamin/agent-harness/commit/a906ee951d6dfa5aa74e149f2e41fa214ffad2cf))
130
+ * **kilocode:** preserve unreconstructable totals with result extras ([7585e29](https://github.com/viamin/agent-harness/commit/7585e29d0eb6668f48b6db3ab033272b74d4831f))
131
+ * **kilocode:** preserve whitespace in text alias chunks ([a6592c1](https://github.com/viamin/agent-harness/commit/a6592c12e753a91cdd8f6c0041100c2a0c01d89a))
132
+ * **kilocode:** read terminal result text from hash payloads ([2ac7012](https://github.com/viamin/agent-harness/commit/2ac701278133e2f0fee64a7cb33f854ecff0754b))
133
+ * **kilocode:** recompute totals from updated usage ([aafabfd](https://github.com/viamin/agent-harness/commit/aafabfd972c56e35bcaa36eb92b1658cb6579a6c))
134
+ * **kilocode:** reject fractional token counts ([935a41d](https://github.com/viamin/agent-harness/commit/935a41d327f7d9433368db40da93ab1983e14cbc))
135
+ * **kilocode:** reject non-decimal string token counts ([0be6ddf](https://github.com/viamin/agent-harness/commit/0be6ddf31a306face9b61498d1a77d08a94a02a5))
136
+ * **kilocode:** remove stray binstubs and add missing test coverage ([25287f7](https://github.com/viamin/agent-harness/commit/25287f736c8bd81820553fc5ca2517e1c64f261c))
137
+ * **kilocode:** replace stale partial extra usage fields ([4b89027](https://github.com/viamin/agent-harness/commit/4b8902768620ffb5787e194753b14f6eb7c496d7))
138
+ * **kilocode:** skip scalar JSON lines before reading event fields ([b8fce27](https://github.com/viamin/agent-harness/commit/b8fce27bceb998a6a4b2a3de7ed95f96a2794c3d))
139
+ * **kilocode:** support hash-shaped part text aliases ([778e25c](https://github.com/viamin/agent-harness/commit/778e25c62cf32bafd1c0fb4bae32b2ea13cf1df9))
140
+ * **kilocode:** support nested result message aliases ([e8a5988](https://github.com/viamin/agent-harness/commit/e8a5988e34dfb1afd70eccb551985fa7e273192e))
141
+ * **kilocode:** support result text aliases ([63ccff9](https://github.com/viamin/agent-harness/commit/63ccff9be0c32ffcfd04e3745a441d6d2370eeac))
142
+ * **kilocode:** support scalar part structured errors ([2dd4740](https://github.com/viamin/agent-harness/commit/2dd47408f39340b075815ce64ac02c588fc445de))
143
+ * **kilocode:** support scalar text part payloads ([b9e9013](https://github.com/viamin/agent-harness/commit/b9e90137e6b43fb2eb789857da319221203a7f76))
144
+ * **kilocode:** support text event alias payloads ([4c425ff](https://github.com/viamin/agent-harness/commit/4c425fff93dcd872e2640743cb21da9668f952b7))
145
+ * **kilocode:** support top-level result text aliases ([9ce7c02](https://github.com/viamin/agent-harness/commit/9ce7c0249acaf3b44731271fc8b50d8fbb14cedc))
146
+ * **kilocode:** support top-level structured error text ([cad1d31](https://github.com/viamin/agent-harness/commit/cad1d31007d79b75daecb61b4f1ed8573f9e70a0))
147
+ * **kilocode:** suppress raw ndjson output for structured events ([d7a07d7](https://github.com/viamin/agent-harness/commit/d7a07d70812b40a75f6a09924d1fd609e99a61df))
148
+ * **kilocode:** treat missing usage tokens as unknown ([5e6335f](https://github.com/viamin/agent-harness/commit/5e6335f901e8f197de9c8f4df5727f90fc06ee43))
149
+ * **kilocode:** whitelist structured event types ([d6d8a7d](https://github.com/viamin/agent-harness/commit/d6d8a7d5bf6268050bf2083494360debeec80fab))
150
+
151
+
152
+ ### Improvements
153
+
154
+ * **kilocode:** remove unreachable structured error branch ([3d7ec3d](https://github.com/viamin/agent-harness/commit/3d7ec3d94e7be6b6f2c2c92d0daf0ce0590c4067))
155
+
156
+ ## [0.6.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.9...agent-harness/v0.6.0) (2026-04-12)
157
+
158
+
159
+ ### Features
160
+
161
+ * **aider:** extract token usage via --llm-history-file ([0fff343](https://github.com/viamin/agent-harness/commit/0fff343f943d93899d0222b16ffa9832611289ff)), closes [#100](https://github.com/viamin/agent-harness/issues/100)
162
+
3
163
  ## [0.5.9](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.8...agent-harness/v0.5.9) (2026-04-12)
4
164
 
5
165
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+ require "shellwords"
5
+ require "tmpdir"
6
+
3
7
  module AgentHarness
4
8
  module Providers
5
9
  # Aider AI coding assistant provider
@@ -192,29 +196,382 @@ module AgentHarness
192
196
  ["--restore-chat-history", session_id]
193
197
  end
194
198
 
199
+ def send_message(prompt:, **options)
200
+ log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
201
+
202
+ options = normalize_provider_runtime(options)
203
+ runtime = options[:provider_runtime]
204
+
205
+ options = normalize_mcp_servers(options)
206
+ validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
207
+
208
+ llm_history_path = generate_llm_history_path
209
+ command = build_command(prompt, options.merge(llm_history_path: llm_history_path))
210
+ preparation = build_execution_preparation(options)
211
+ timeout = options[:timeout] || @config.timeout || default_timeout
212
+
213
+ start_time = Time.now
214
+ result = execute_with_timeout(
215
+ command,
216
+ timeout: timeout,
217
+ env: build_env(options),
218
+ preparation: preparation,
219
+ **command_execution_options(options)
220
+ )
221
+ duration = Time.now - start_time
222
+
223
+ response = parse_response(result, duration: duration, llm_history_path: llm_history_path)
224
+ if runtime&.model
225
+ response = Response.new(
226
+ output: response.output,
227
+ exit_code: response.exit_code,
228
+ duration: response.duration,
229
+ provider: response.provider,
230
+ model: runtime.model,
231
+ tokens: response.tokens,
232
+ metadata: response.metadata,
233
+ error: response.error
234
+ )
235
+ end
236
+
237
+ track_tokens(response) if response.tokens
238
+
239
+ log_debug("send_message_complete", duration: duration, tokens: response.tokens)
240
+
241
+ response
242
+ rescue McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
243
+ raise
244
+ rescue => e
245
+ handle_error(e, prompt: prompt, options: options)
246
+ ensure
247
+ cleanup_llm_history_file!(llm_history_path)
248
+ end
249
+
195
250
  protected
196
251
 
197
252
  def build_command(prompt, options)
198
253
  cmd = [self.class.binary_name]
254
+ runtime = options[:provider_runtime]
199
255
 
200
- # Run in non-interactive mode
201
256
  cmd << "--yes"
202
257
 
203
- if @config.model && !@config.model.empty?
204
- cmd += ["--model", @config.model]
258
+ if options[:llm_history_path]
259
+ cmd += ["--llm-history-file", options[:llm_history_path]]
260
+ end
261
+
262
+ model = runtime&.model || @config.model
263
+ if model && !model.empty?
264
+ cmd += ["--model", model]
205
265
  end
206
266
 
207
267
  if options[:session]
208
268
  cmd += session_flags(options[:session])
209
269
  end
210
270
 
271
+ if runtime&.flags&.any?
272
+ validate_runtime_flags!(runtime.flags)
273
+ cmd += runtime.flags
274
+ end
275
+
211
276
  cmd += ["--message", prompt]
212
277
 
213
278
  cmd
214
279
  end
215
280
 
281
+ def parse_response(result, duration:, llm_history_path: nil)
282
+ response = super(result, duration: duration)
283
+ tokens = parse_token_usage(result, llm_history_path: llm_history_path)
284
+
285
+ return response unless tokens
286
+
287
+ Response.new(
288
+ output: response.output,
289
+ exit_code: response.exit_code,
290
+ duration: response.duration,
291
+ provider: response.provider,
292
+ model: response.model,
293
+ tokens: tokens,
294
+ metadata: response.metadata,
295
+ error: response.error
296
+ )
297
+ end
298
+
216
299
  def default_timeout
217
- 600 # Aider can take longer
300
+ 600
301
+ end
302
+
303
+ private
304
+
305
+ TOKEN_COUNT_PATTERN = /\d[\d,]*(?:\.\d+)?[kmb]?/i
306
+
307
+ TOKEN_USAGE_PATTERN =
308
+ /^\s*Tokens:\s*(?<input>#{TOKEN_COUNT_PATTERN})\s+sent(?:,\s*#{TOKEN_COUNT_PATTERN}\s+cache\s+\w+)*,\s*(?<output>#{TOKEN_COUNT_PATTERN})\s+received\.?(?:\s+Cost:\s+.+)?\s*$/i
309
+ FOOTER_COST_PATTERN = /^\s*Cost:\s+.+\s*$/i
310
+ RUN_SHELL_COMMAND_PATTERN = /^\s*Run shell command\?.*$/i
311
+ OUTPUT_STATUS_PATTERN =
312
+ /^\s*(?:Applied edit to|Commit\b|Committing\b|You can use \/undo\b|Added .+ to the chat\.|Removed .+ from the chat\.|Use \/help\b|Create new file\?|Allow edits to\b|Edit the files\?|Run shell command\?).*$/i
313
+ OUTPUT_PATH_PATTERN = /\A(?:\.\.?\/|\/|~\/)[\w.\-\/]+\z/
314
+ OUTPUT_DOTFILE_PATTERN = /\A\.[\w.-]+\z/
315
+ OUTPUT_FILENAME_PATTERN = /\A[\w.-]+\.[A-Za-z][\w.-]*\z/
316
+ COMMON_SHELL_COMMAND_PATTERN =
317
+ /\A(?:git|bundle|ruby|python\d*(?:\.\d+)?|uv|npm|yarn|pnpm|node|bash|sh|zsh|make|rake|rspec|rails|go|pytest|bin\/[\w.-]+|sed|rg|grep|find|ls|cat|cp|mv|rm|mkdir|touch|chmod|chown|docker|kubectl)\z/
318
+ EXECUTOR_LLM_HISTORY_TIMEOUT = 10
319
+
320
+ def generate_llm_history_path
321
+ return "/tmp/aider_llm_history_#{Process.pid}_#{SecureRandom.hex(8)}" if sandboxed_environment?
322
+
323
+ File.join(Dir.tmpdir, "aider_llm_history_#{Process.pid}_#{SecureRandom.hex(8)}")
324
+ end
325
+
326
+ def parse_token_usage(result, llm_history_path:)
327
+ # Aider 0.86.x writes --llm-history-file as conversation text, not JSONL.
328
+ # Prefer the request-local history file when it includes a token report,
329
+ # but fall back to captured command output because the usage summary is
330
+ # printed there during normal runs.
331
+ parse_token_usage_text(safe_read_llm_history(llm_history_path), source: :history) ||
332
+ parse_token_usage_text(result.stdout, source: :output) ||
333
+ parse_token_usage_text(result.stderr, source: :output)
334
+ end
335
+
336
+ def read_llm_history(path)
337
+ return read_executor_llm_history(path) if sandboxed_environment?
338
+ return nil unless path && File.exist?(path) && !File.zero?(path)
339
+
340
+ content = File.read(path)
341
+ return nil if content.strip.empty?
342
+
343
+ content
344
+ end
345
+
346
+ def safe_read_llm_history(path)
347
+ read_llm_history(path)
348
+ rescue => e
349
+ log_debug("llm_history_parse_error", error: e.message)
350
+ nil
351
+ end
352
+
353
+ def parse_token_usage_text(content, source: :output)
354
+ return nil if content.nil? || content.strip.empty?
355
+
356
+ match = if source == :history
357
+ extract_history_token_usage_match(content)
358
+ else
359
+ extract_output_token_usage_match(content)
360
+ end
361
+ return nil unless match
362
+
363
+ input = parse_token_count(match[:input])
364
+ output = parse_token_count(match[:output])
365
+
366
+ {input: input, output: output, total: input + output}
367
+ end
368
+
369
+ def extract_history_token_usage_match(content)
370
+ lines = content.lines
371
+
372
+ lines.each_index.reverse_each do |index|
373
+ match = TOKEN_USAGE_PATTERN.match(lines[index])
374
+ next unless match
375
+ next unless history_token_usage_footer_line?(lines, index)
376
+
377
+ return match
378
+ end
379
+
380
+ nil
381
+ end
382
+
383
+ def extract_output_token_usage_match(content)
384
+ lines = content.lines
385
+
386
+ lines.each_index.reverse_each do |index|
387
+ match = TOKEN_USAGE_PATTERN.match(lines[index])
388
+ next unless match
389
+ next unless output_token_usage_footer_line?(lines, index)
390
+
391
+ return match
392
+ end
393
+
394
+ nil
395
+ end
396
+
397
+ def history_token_usage_footer_line?(lines, index)
398
+ footer_prefix?(lines, index) && footer_suffix?(lines, index)
399
+ end
400
+
401
+ def output_token_usage_footer_line?(lines, index)
402
+ footer_prefix?(lines, index) && output_footer_suffix?(lines, index)
403
+ end
404
+
405
+ def footer_prefix?(lines, index)
406
+ block_start = index
407
+ while block_start.positive? && TOKEN_USAGE_PATTERN.match?(lines[block_start - 1])
408
+ block_start -= 1
409
+ end
410
+
411
+ return false if block_start.zero?
412
+
413
+ lines[block_start - 1].strip.empty?
414
+ end
415
+
416
+ def footer_suffix?(lines, index)
417
+ lines[(index + 1)..].to_a.all? do |line|
418
+ stripped = line.strip
419
+ stripped.empty? || TOKEN_USAGE_PATTERN.match?(line) || FOOTER_COST_PATTERN.match?(line)
420
+ end
421
+ end
422
+
423
+ def output_footer_suffix?(lines, index)
424
+ suffix_lines = lines[(index + 1)..].to_a
425
+ shell_prompt_index = suffix_lines.index { |line| RUN_SHELL_COMMAND_PATTERN.match?(line) }
426
+
427
+ suffix_lines.each_with_index.all? do |line, line_index|
428
+ stripped = line.strip
429
+ stripped.empty? ||
430
+ TOKEN_USAGE_PATTERN.match?(line) ||
431
+ FOOTER_COST_PATTERN.match?(line) ||
432
+ OUTPUT_STATUS_PATTERN.match?(line) ||
433
+ output_path_footer_line?(stripped) ||
434
+ output_command_footer_line?(line, line_index, shell_prompt_index)
435
+ end
436
+ end
437
+
438
+ def output_path_footer_line?(line)
439
+ OUTPUT_PATH_PATTERN.match?(line) ||
440
+ OUTPUT_DOTFILE_PATTERN.match?(line) ||
441
+ OUTPUT_FILENAME_PATTERN.match?(line) ||
442
+ (line.include?("/") && line.match?(/\A[\w.\-\/]+\z/))
443
+ end
444
+
445
+ def output_command_footer_line?(line, line_index, shell_prompt_index)
446
+ return false unless shell_prompt_index && line_index < shell_prompt_index
447
+
448
+ stripped = line.strip
449
+ return false if stripped.end_with?(".", "?", "!")
450
+ return false if stripped.empty?
451
+
452
+ tokens = shell_command_footer_tokens(stripped)
453
+ return false if tokens.empty?
454
+ command = tokens.first
455
+ return false unless command_invocation_token?(command)
456
+ return single_token_command_footer?(command) if tokens.length == 1
457
+ return false unless command_line_token?(command, tokens[1..])
458
+
459
+ tokens[1..].all? { |token| command_argument_token?(token) }
460
+ end
461
+
462
+ def shell_command_footer_tokens(line)
463
+ Shellwords.shellsplit(line.sub(/\A[$>#]\s*/, ""))
464
+ rescue ArgumentError
465
+ []
466
+ end
467
+
468
+ def command_token?(token)
469
+ token.match?(/\A[a-z0-9_][\w.\/~:-]*\z/) && token.match?(/[a-z]/)
470
+ end
471
+
472
+ def command_invocation_token?(token)
473
+ command_token?(token) || executable_path_token?(token)
474
+ end
475
+
476
+ def executable_path_token?(token)
477
+ token.match?(%r{\A(?:\.\.?/|/|~/)[\w.+%:@=-][\w./+%:@~=-]*\z})
478
+ end
479
+
480
+ def command_line_token?(token, arguments)
481
+ command_invocation_token?(token) &&
482
+ (COMMON_SHELL_COMMAND_PATTERN.match?(token) ||
483
+ executable_path_token?(token) ||
484
+ command_footer_shell_like_arguments?(arguments))
485
+ end
486
+
487
+ def single_token_command_footer?(token)
488
+ COMMON_SHELL_COMMAND_PATTERN.match?(token) || executable_path_token?(token)
489
+ end
490
+
491
+ def command_footer_shell_like_arguments?(arguments)
492
+ arguments.any? do |argument|
493
+ argument.match?(%r{\A(?:&&|\|\|?|\||[<>]|>>|&>|2>)\z}) ||
494
+ argument.start_with?("-", "./", "../", "/", "~/") ||
495
+ argument.include?("/")
496
+ end
497
+ end
498
+
499
+ def command_argument_token?(token)
500
+ !token.empty? && !token.match?(/[[:cntrl:]]/)
501
+ end
502
+
503
+ def parse_token_count(value)
504
+ normalized = value.delete(",").downcase
505
+ multiplier = case normalized[-1]
506
+ when "k" then 1_000
507
+ when "m" then 1_000_000
508
+ when "b" then 1_000_000_000
509
+ else 1
510
+ end
511
+ normalized = normalized[0...-1] if multiplier > 1
512
+
513
+ (normalized.to_f * multiplier).round
514
+ end
515
+
516
+ def cleanup_llm_history_file!(path)
517
+ return unless path
518
+
519
+ return cleanup_executor_llm_history_file!(path) if sandboxed_environment?
520
+
521
+ File.delete(path) if File.exist?(path)
522
+ rescue => e
523
+ log_debug("llm_history_cleanup_error", error: e.message)
524
+ nil
525
+ end
526
+
527
+ def validate_runtime_flags!(flags)
528
+ invalid_flags = reserved_runtime_flags(flags)
529
+ return if invalid_flags.empty?
530
+
531
+ raise ArgumentError,
532
+ "Aider provider_runtime.flags cannot override provider-managed flags: " \
533
+ "#{invalid_flags.join(", ")}"
534
+ end
535
+
536
+ def reserved_runtime_flags(flags)
537
+ flags.each_with_index.filter_map do |flag, index|
538
+ next unless reserved_runtime_flag?(flag)
539
+
540
+ if flag == "--llm-history-file" && flags[index + 1]
541
+ "#{flag} #{flags[index + 1]}"
542
+ else
543
+ flag
544
+ end
545
+ end.uniq
546
+ end
547
+
548
+ def reserved_runtime_flag?(flag)
549
+ flag == "--llm-history-file" || flag.start_with?("--llm-history-file=")
550
+ end
551
+
552
+ def read_executor_llm_history(path)
553
+ return nil unless path
554
+
555
+ result = @executor.execute(
556
+ ["sh", "-lc", "if [ -s #{Shellwords.escape(path)} ]; then cat #{Shellwords.escape(path)}; fi"],
557
+ timeout: EXECUTOR_LLM_HISTORY_TIMEOUT
558
+ )
559
+ return nil unless result.success?
560
+
561
+ content = result.stdout
562
+ return nil if content.to_s.strip.empty?
563
+
564
+ content
565
+ end
566
+
567
+ def cleanup_executor_llm_history_file!(path)
568
+ @executor.execute(
569
+ ["sh", "-lc", "rm -f -- #{Shellwords.escape(path)}"],
570
+ timeout: EXECUTOR_LLM_HISTORY_TIMEOUT
571
+ )
572
+ rescue => e
573
+ log_debug("llm_history_cleanup_error", error: e.message)
574
+ nil
218
575
  end
219
576
  end
220
577
  end
@@ -10,6 +10,29 @@ module AgentHarness
10
10
  class Codex < Base
11
11
  SUPPORTED_CLI_VERSION = "0.116.0"
12
12
  SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.117.0").freeze
13
+ OAUTH_REFRESH_FAILURE_PATTERNS = [
14
+ /refresh_token_reused/i,
15
+ /failed to refresh token\b.*\b401\b/im,
16
+ /failed to refresh token\b.*unauthorized/im,
17
+ /failed to refresh token\b.*\binvalid_client\b/im,
18
+ /failed to refresh token\b.*\binvalid_grant\b/im,
19
+ /failed to refresh token\b.*invalid.*refresh.*token/im,
20
+ /failed to refresh token\b.*refresh.*token.*invalid/im,
21
+ /your access token could not be refreshed because\b.*\b401\b/im,
22
+ /your access token could not be refreshed because\b.*unauthorized/im,
23
+ /your access token could not be refreshed because\b.*\binvalid_client\b/im,
24
+ /your access token could not be refreshed because\b.*\binvalid_grant\b/im,
25
+ /your access token could not be refreshed because\b.*invalid.*refresh.*token/im,
26
+ /your access token could not be refreshed because\b.*refresh.*token.*invalid/im,
27
+ /your access token could not be refreshed because\s+your refresh token .*already (?:been )?used/im,
28
+ /refresh token .*already (?:been )?used/im
29
+ ].freeze
30
+ OAUTH_REFRESH_TRANSIENT_PATTERNS = [
31
+ /your access token could not be refreshed because\s+(?:the\s+)?auth(?:entication)? service(?:\s+(?:is|was))?\s+(?:temporarily\s+)?unavailable/im,
32
+ /your access token could not be refreshed because .*connection.*error/im,
33
+ /failed to refresh token\b.*connection.*error/im,
34
+ /failed to refresh token\b.*service(?:\s+(?:is|was))?\s+(?:temporarily\s+)?unavailable/im
35
+ ].freeze
13
36
 
14
37
  class << self
15
38
  def provider_name
@@ -171,15 +194,26 @@ module AgentHarness
171
194
  end
172
195
 
173
196
  def error_patterns
174
- COMMON_ERROR_PATTERNS.merge(
175
- auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [/\b401\b/, /incorrect.*api.*key/i],
176
- transient: COMMON_ERROR_PATTERNS[:transient] + [/connection.*reset/i],
197
+ {
198
+ rate_limited: COMMON_ERROR_PATTERNS[:rate_limited],
199
+ timeout: [
200
+ /your access token could not be refreshed.*(?:timeout|timed.?out)/im,
201
+ /failed to refresh token\b.*(?:timeout|timed.?out)/im
202
+ ],
203
+ transient: COMMON_ERROR_PATTERNS[:transient] + [
204
+ /connection.*reset/i
205
+ ] + OAUTH_REFRESH_TRANSIENT_PATTERNS,
206
+ auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [
207
+ /\b401\b/,
208
+ /incorrect.*api.*key/i
209
+ ] + OAUTH_REFRESH_FAILURE_PATTERNS,
210
+ quota_exceeded: COMMON_ERROR_PATTERNS[:quota_exceeded],
177
211
  sandbox_failure: [
178
212
  /bwrap.*no permissions/i,
179
213
  /no permissions to create a new namespace/i,
180
214
  /unprivileged.*namespace/i
181
215
  ]
182
- )
216
+ }
183
217
  end
184
218
 
185
219
  def auth_status