@@ -30,6 +30,7 @@ class WP_Agent_Conversation_Result {
3030 public const OUTCOME_STOP_NATURAL = WP_Agent_Run_Outcome::STOP_NATURAL ;
3131 public const OUTCOME_STOP_MAX_TURNS = WP_Agent_Run_Outcome::STOP_MAX_TURNS ;
3232 public const OUTCOME_STOP_PROVIDER_ERROR = WP_Agent_Run_Outcome::STOP_PROVIDER_ERROR ;
33+ public const TOOL_OBSERVABILITY_VERSION = 1 ;
3334
3435 /**
3536 * Validate and normalize a loop result.
@@ -252,11 +253,141 @@ public static function normalize( array $result ): array {
252253 throw self ::invalid ( 'runtime_tool_pending ' , 'must be an array when present ' );
253254 }
254255
256+ $ result ['tool_observability ' ] = self ::tool_observability ( $ result ['tool_events ' ], $ result ['tool_execution_results ' ] );
255257 $ result ['run_outcome ' ] = WP_Agent_Run_Outcome::normalize ( $ result ['run_outcome ' ] ?? null , $ result );
256258
257259 return $ result ;
258260 }
259261
262+ /**
263+ * Project internal tool lifecycle data to a compact content-redacted contract.
264+ *
265+ * @param array<mixed> $events Ordered tool lifecycle events.
266+ * @param array<mixed> $results Tool execution results.
267+ * @return array{version:int,calls:array<int,array<string,mixed>>}
268+ */
269+ private static function tool_observability ( array $ events , array $ results ): array {
270+ $ calls = array ();
271+ $ pending = array ();
272+ $ result_uses = array ();
273+
274+ foreach ( $ events as $ event ) {
275+ if ( ! is_array ( $ event ) ) {
276+ continue ;
277+ }
278+
279+ $ type = $ event ['type ' ] ?? '' ;
280+ $ tool_call_id = $ event ['tool_call_id ' ] ?? '' ;
281+ if ( ! is_string ( $ type ) || ! is_string ( $ tool_call_id ) || '' === $ tool_call_id ) {
282+ continue ;
283+ }
284+
285+ if ( 'tool_call ' === $ type ) {
286+ $ metadata = is_array ( $ event ['metadata ' ] ?? null ) ? $ event ['metadata ' ] : array ();
287+ $ parameters = is_array ( $ metadata ['parameters ' ] ?? null ) ? $ metadata ['parameters ' ] : array ();
288+ $ keys = array_map ( 'strval ' , array_keys ( $ parameters ) );
289+ $ calls [] = array (
290+ 'sequence ' => count ( $ calls ) + 1 ,
291+ 'turn ' => is_int ( $ event ['turn_count ' ] ?? null ) ? $ event ['turn_count ' ] : 0 ,
292+ 'tool_call_id ' => $ tool_call_id ,
293+ 'tool_name ' => is_string ( $ event ['tool_name ' ] ?? null ) ? $ event ['tool_name ' ] : '' ,
294+ 'status ' => 'pending ' ,
295+ 'arguments ' => array (
296+ 'keys ' => $ keys ,
297+ 'count ' => count ( $ keys ),
298+ 'redacted ' => true ,
299+ ),
300+ );
301+ $ pending [ $ tool_call_id ][] = count ( $ calls ) - 1 ;
302+ continue ;
303+ }
304+
305+ if ( ! in_array ( $ type , array ( 'tool_result ' , 'pending ' ), true ) || empty ( $ pending [ $ tool_call_id ] ) ) {
306+ continue ;
307+ }
308+
309+ if ( 'pending ' === $ type ) {
310+ continue ;
311+ }
312+ $ call_index = (int ) array_shift ( $ pending [ $ tool_call_id ] );
313+
314+ $ metadata = is_array ( $ event ['metadata ' ] ?? null ) ? $ event ['metadata ' ] : array ();
315+ $ rejected = true === ( $ metadata ['rejected ' ] ?? false );
316+ $ succeeded = true === ( $ metadata ['success ' ] ?? false );
317+ $ calls [ $ call_index ]['status ' ] = $ rejected ? 'rejected ' : ( $ succeeded ? 'succeeded ' : 'failed ' );
318+
319+ $ result = self ::matching_tool_result ( $ results , $ tool_call_id , $ result_uses );
320+ if ( is_array ( $ result ) ) {
321+ $ execution_result = is_array ( $ result ['result ' ] ?? null ) ? $ result ['result ' ] : array ();
322+ $ canonical_name = $ execution_result ['tool_name ' ] ?? null ;
323+ if ( is_string ( $ canonical_name ) && '' !== $ canonical_name ) {
324+ $ calls [ $ call_index ]['tool_name ' ] = $ canonical_name ;
325+ }
326+
327+ if ( $ succeeded && array_key_exists ( 'result ' , $ execution_result ) ) {
328+ $ calls [ $ call_index ]['result ' ] = self ::result_shape ( $ execution_result ['result ' ] );
329+ }
330+ }
331+
332+ if ( ! $ succeeded ) {
333+ $ calls [ $ call_index ]['error ' ] = $ rejected
334+ ? array ( 'code ' => 'agents_api_tool_call_rejected ' , 'message ' => 'Tool call was rejected. ' )
335+ : array ( 'code ' => 'agents_api_tool_execution_failed ' , 'message ' => 'Tool execution failed. ' );
336+ }
337+ }
338+
339+ return array (
340+ 'version ' => self ::TOOL_OBSERVABILITY_VERSION ,
341+ 'calls ' => $ calls ,
342+ );
343+ }
344+
345+ /**
346+ * Find the next execution result for a tool-call id.
347+ *
348+ * @param array<mixed> $results Tool results.
349+ * @param string $tool_call_id Tool call id.
350+ * @param array<string, int> $uses Per-id result offsets.
351+ * @return array<mixed>|null
352+ */
353+ private static function matching_tool_result ( array $ results , string $ tool_call_id , array &$ uses ): ?array {
354+ $ offset = $ uses [ $ tool_call_id ] ?? 0 ;
355+ $ match = 0 ;
356+ foreach ( $ results as $ result ) {
357+ if ( ! is_array ( $ result ) || $ tool_call_id !== ( $ result ['tool_call_id ' ] ?? null ) ) {
358+ continue ;
359+ }
360+ if ( $ match === $ offset ) {
361+ $ uses [ $ tool_call_id ] = $ offset + 1 ;
362+ return $ result ;
363+ }
364+ ++$ match ;
365+ }
366+
367+ return null ;
368+ }
369+
370+ /**
371+ * Describe a result without exposing its content.
372+ *
373+ * @param mixed $value Result value.
374+ * @return array<string,int|string>
375+ */
376+ private static function result_shape ( $ value ): array {
377+ if ( is_array ( $ value ) ) {
378+ return array (
379+ 'type ' => array_is_list ( $ value ) ? 'array ' : 'object ' ,
380+ 'count ' => count ( $ value ),
381+ );
382+ }
383+
384+ if ( is_string ( $ value ) ) {
385+ return array ( 'type ' => 'string ' , 'size ' => strlen ( $ value ) );
386+ }
387+
388+ return array ( 'type ' => strtolower ( gettype ( $ value ) ) );
389+ }
390+
260391 /**
261392 * Build a machine-readable validation exception.
262393 *
0 commit comments