PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/agents/runner.php +83 -11 1.1.4 → 1.1.12 View file →
@@ -67,8 +67,20 @@
67 67 */
68 68 const OPENSTATION_AGENT_RUNNER_MAX_TURNS = 8;
69 69
70 70 /**
71 + * Consecutive turns in which every tool call failed with the same
72 + * errors before the loop gives up on tools and asks for a final answer.
73 + *
74 + * A model reads a tool error and usually fixes its next call. One that
75 + * sends the same failing call a third time is not going to fix it on
76 + * the eighth: the Localizer once spent seven turns re-sending a
77 + * `create_post` call the ability rejected identically every time. Three
78 + * is one honest correction attempt plus proof it did not help.
79 + */
80 +const OPENSTATION_AGENT_RUNNER_STUCK_TURNS = 3;
81 +
82 +/**
71 83 * Seconds to allow one provider generation request, replacing the
72 84 * WordPress HTTP default of 5.
73 85 *
74 86 * The AI Client's HTTP adapter issues provider calls through
@@ -504,10 +516,15 @@
504 516 'text' => (string) $message,
505 517 );
506 518 $tool_trace = array();
507 519
520 + $turns_used = 0;
521 + $last_failure = '';
522 + $repeated_failures = 0;
523 +
508 524 for ( $turn = 1; $turn <= OPENSTATION_AGENT_RUNNER_MAX_TURNS; $turn++ ) {
509 - $generated = openstation_agent_runner_generate( $agent_user_id, $history, $tool_defs, $instructions );
525 + $turns_used = $turn;
526 + $generated = openstation_agent_runner_generate( $agent_user_id, $history, $tool_defs, $instructions );
510 527 if ( is_wp_error( $generated ) && openstation_agent_generate_error_is_transient( $generated ) ) {
511 528 // One bounded retry for provider-side hiccups (a failed
512 529 // models-list fetch, a gateway timeout, a borderline
513 530 // refusal). A manual "try again" was already the working
@@ -609,15 +626,27 @@
609 626 $history[] = array(
610 627 'type' => 'tool_results',
611 628 'results' => $results,
612 629 );
630 +
631 + $failure = openstation_agent_runner_failure_signature( $results );
632 + if ( '' !== $failure && $failure === $last_failure ) {
633 + ++$repeated_failures;
634 + } else {
635 + $repeated_failures = '' === $failure ? 0 : 1;
636 + }
637 + $last_failure = $failure;
638 + if ( $repeated_failures >= OPENSTATION_AGENT_RUNNER_STUCK_TURNS ) {
639 + break;
640 + }
613 641 }
614 642
615 - // Cap reached with the model still asking for tools. Force one
616 - // last TOOL-LESS generate over the transcript so far: with nothing
617 - // to call, the model can only produce a final answer from what it
618 - // already gathered. A best-effort summary beats discarding the
619 - // whole run (observed on Anthropic: a model happily spends the cap
643 + // Cap reached (or the model stuck re-sending the same failing
644 + // call) with it still asking for tools. Force one last TOOL-LESS
645 + // generate over the transcript so far: with nothing to call, the
646 + // model can only produce a final answer from what it already
647 + // gathered. A best-effort summary beats discarding the whole run
648 + // (observed on Anthropic: a model happily spends the cap
620 649 // re-searching before it answers).
621 650 $generated = openstation_agent_runner_generate( $agent_user_id, $history, array(), $instructions );
622 651 if ( is_wp_error( $generated ) && openstation_agent_generate_error_is_transient( $generated ) ) {
623 652 $generated = openstation_agent_runner_generate( $agent_user_id, $history, array(), $instructions );
@@ -629,9 +658,9 @@
629 658 return array(
630 659 'text' => $answer['text'],
631 660 'callToActions' => $answer['callToActions'],
632 661 'toolCalls' => $tool_trace,
633 - 'turns' => OPENSTATION_AGENT_RUNNER_MAX_TURNS + 1,
662 + 'turns' => $turns_used + 1,
634 663 );
635 664 }
636 665
637 666 return new WP_Error(
@@ -636,16 +665,45 @@
636 665
637 666 return new WP_Error(
638 667 'openstation_agent_runner_max_turns',
639 668 sprintf(
640 - /* translators: %d is the max-turn cap. */
669 + /* translators: %d is the number of turns the run made. */
641 670 __( 'Agent stopped after %d turns without a final answer.', 'desktop-mode' ),
642 - OPENSTATION_AGENT_RUNNER_MAX_TURNS
671 + $turns_used
643 672 )
644 673 );
645 674 }
646 675
647 676 /**
677 + * Fingerprint of a turn in which every tool call failed: the sorted
678 + * tool names with their error messages. Empty when any call succeeded,
679 + * so a turn that got something done never counts as stuck.
680 + *
681 + * Arguments are deliberately left out. The failure that motivated this
682 + * had the model vary a title between attempts while the ability
683 + * rejected each one for the same missing field, and that IS the same
684 + * failure.
685 + *
686 + * @param array $results Tool results of one turn (`{ name, response }` rows).
687 + * @return string
688 + */
689 +function openstation_agent_runner_failure_signature( array $results ) {
690 + if ( empty( $results ) ) {
691 + return '';
692 + }
693 + $failures = array();
694 + foreach ( $results as $row ) {
695 + $response = isset( $row['response'] ) ? $row['response'] : null;
696 + if ( ! is_array( $response ) || ! isset( $response['error'] ) ) {
697 + return '';
698 + }
699 + $failures[] = ( isset( $row['name'] ) ? (string) $row['name'] : '' ) . "\0" . (string) $response['error'];
700 + }
701 + sort( $failures );
702 + return implode( "\n", $failures );
703 +}
704 +
705 +/**
648 706 * JSON Schema every agent's FINAL answer is constrained to (via the
649 707 * AI Client's structured output, `as_json_response()`): the markdown
650 708 * answer in `text`, plus optional `call_to_actions` the chat renders
651 709 * as buttons when the agent needs the user's confirmation instead of
@@ -896,8 +954,19 @@
896 954 'detail' => $error->get_error_message(),
897 955 )
898 956 );
899 957 }
958 + if ( 'openstation_ai_output_truncated' === $error->get_error_code() ) {
959 + $data = $error->get_error_data();
960 + return new WP_Error(
961 + 'openstation_agent_output_truncated',
962 + __( 'The reply ran past the output-token limit before it finished, so it was discarded rather than acted on incomplete. Ask for something shorter, or raise max_tokens with the openstation_ai_model_config filter.', 'desktop-mode' ),
963 + array(
964 + 'status' => 502,
965 + 'detail' => is_array( $data ) && isset( $data['detail'] ) ? (string) $data['detail'] : '',
966 + )
967 + );
968 + }
900 969 if ( 'openstation_ai_empty_answer' === $error->get_error_code() ) {
901 970 $data = $error->get_error_data();
902 971 return new WP_Error(
903 972 'openstation_agent_empty_answer',
@@ -1243,10 +1312,13 @@
1243 1312 return $ability->execute( $args );
1244 1313 }
1245 1314
1246 1315 /**
1247 - * Append one invocation to the agent's persistent log. Most-recent
1248 - * entries surface in the chat window's history strip.
1316 + * Append one invocation to the agent's persistent log: an audit trail
1317 + * of who ran the agent and what came back, capped at
1318 + * OPENSTATION_AGENT_RUNNER_LOG_CAP entries and readable from PHP with
1319 + * {@see openstation_agent_runner_get_log()}. No UI shows it; the chat
1320 + * window's history is the human's saved conversations, not this log.
1249 1321 *
1250 1322 * @param int $agent_user_id Agent user id.
1251 1323 * @param string $message Submitted message.
1252 1324 * @param array $result `{ text, toolCalls, turns }`.