Skip to content

Commit 969cf55

Browse files
authored
feat(approvals): add pending action resolver contracts
Add generic accepted/rejected approval decision vocabulary and contract-only pending-action handler/resolver interfaces, with smoke coverage wired into the Composer test suite.
1 parent 7b8c51a commit 969cf55

6 files changed

Lines changed: 218 additions & 0 deletions

agents-api.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
require_once AGENTS_API_PATH . 'src/Identity/MaterializedAgentIdentityStoreInterface.php';
4242
require_once AGENTS_API_PATH . 'src/Transcripts/ConversationTranscriptStoreInterface.php';
4343
require_once AGENTS_API_PATH . 'src/Approvals/PendingActionStoreInterface.php';
44+
require_once AGENTS_API_PATH . 'src/Approvals/ApprovalDecision.php';
45+
require_once AGENTS_API_PATH . 'src/Approvals/PendingActionHandlerInterface.php';
46+
require_once AGENTS_API_PATH . 'src/Approvals/PendingActionResolverInterface.php';
4447
require_once AGENTS_API_PATH . 'src/Runtime/AgentMessageEnvelope.php';
4548
require_once AGENTS_API_PATH . 'src/Runtime/AgentExecutionPrincipal.php';
4649
require_once AGENTS_API_PATH . 'src/Runtime/AgentCompactionItem.php';

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"php tests/action-policy-values-smoke.php",
2020
"php tests/tool-runtime-smoke.php",
2121
"php tests/pending-action-store-contract-smoke.php",
22+
"php tests/approval-resolver-contract-smoke.php",
2223
"php tests/identity-smoke.php",
2324
"php tests/compaction-item-smoke.php",
2425
"php tests/conversation-runner-contracts-smoke.php",

src/Approvals/ApprovalDecision.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/**
3+
* Generic pending-action approval decision.
4+
*
5+
* @package AgentsAPI
6+
*/
7+
8+
namespace AgentsAPI\AI\Approvals;
9+
10+
defined( 'ABSPATH' ) || exit;
11+
12+
/**
13+
* Immutable accept/reject decision for a pending action.
14+
*/
15+
final class ApprovalDecision {
16+
17+
public const ACCEPTED = 'accepted';
18+
public const REJECTED = 'rejected';
19+
20+
/** @var string Normalized decision value. */
21+
private string $value;
22+
23+
/**
24+
* @param string $value Decision value.
25+
*/
26+
private function __construct( string $value ) {
27+
if ( ! in_array( $value, array( self::ACCEPTED, self::REJECTED ), true ) ) {
28+
throw new \InvalidArgumentException( 'Approval decision must be accepted or rejected.' );
29+
}
30+
31+
$this->value = $value;
32+
}
33+
34+
/** @return self Accepted decision. */
35+
public static function accepted(): self {
36+
return new self( self::ACCEPTED );
37+
}
38+
39+
/** @return self Rejected decision. */
40+
public static function rejected(): self {
41+
return new self( self::REJECTED );
42+
}
43+
44+
/**
45+
* Build a decision from a stored or request value.
46+
*
47+
* @param string $value Decision value.
48+
* @return self
49+
*/
50+
public static function from_string( string $value ): self {
51+
return new self( $value );
52+
}
53+
54+
/** @return bool Whether the pending action was accepted. */
55+
public function is_accepted(): bool {
56+
return self::ACCEPTED === $this->value;
57+
}
58+
59+
/** @return bool Whether the pending action was rejected. */
60+
public function is_rejected(): bool {
61+
return self::REJECTED === $this->value;
62+
}
63+
64+
/** @return string Normalized decision value. */
65+
public function value(): string {
66+
return $this->value;
67+
}
68+
69+
/** @return string Normalized decision value. */
70+
public function __toString(): string {
71+
return $this->value;
72+
}
73+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
/**
3+
* Generic pending-action handler contract.
4+
*
5+
* @package AgentsAPI
6+
*/
7+
8+
namespace AgentsAPI\AI\Approvals;
9+
10+
defined( 'ABSPATH' ) || exit;
11+
12+
interface PendingActionHandlerInterface {
13+
14+
/**
15+
* Resolve a stored pending action with a caller-provided decision.
16+
*
17+
* The apply input is the implementation-owned data captured when the action
18+
* was queued. The payload is fresh resolver input supplied with the decision.
19+
* Permission checks, persistence, and transport concerns stay with callers.
20+
*
21+
* @param ApprovalDecision $decision Accepted/rejected decision.
22+
* @param array $apply_input Stored apply input for the pending action.
23+
* @param array $payload Fresh resolver payload supplied with the decision.
24+
* @param array $context Optional caller context.
25+
* @return mixed Generic implementation result.
26+
*/
27+
public function handle_pending_action( ApprovalDecision $decision, array $apply_input, array $payload = array(), array $context = array() ): mixed;
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
* Generic pending-action resolver contract.
4+
*
5+
* @package AgentsAPI
6+
*/
7+
8+
namespace AgentsAPI\AI\Approvals;
9+
10+
defined( 'ABSPATH' ) || exit;
11+
12+
interface PendingActionResolverInterface {
13+
14+
/**
15+
* Resolve a pending action by identifier.
16+
*
17+
* Implementations own lookup and persistence. Callers own authentication and
18+
* authorization before invoking the resolver.
19+
*
20+
* @param string $pending_action_id Stable pending-action identifier.
21+
* @param ApprovalDecision $decision Accepted/rejected decision.
22+
* @param array $payload Fresh resolver payload supplied with the decision.
23+
* @param array $context Optional caller context.
24+
* @return mixed Generic resolver result.
25+
*/
26+
public function resolve_pending_action( string $pending_action_id, ApprovalDecision $decision, array $payload = array(), array $context = array() ): mixed;
27+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Pure-PHP smoke test for pending-action approval resolver contracts.
4+
*
5+
* Run with: php tests/approval-resolver-contract-smoke.php
6+
*
7+
* @package AgentsAPI\Tests
8+
*/
9+
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
define( 'ABSPATH', __DIR__ . '/' );
12+
}
13+
14+
$failures = array();
15+
$passes = 0;
16+
17+
echo "agents-api-approval-resolver-contract-smoke\n";
18+
19+
require_once __DIR__ . '/agents-api-smoke-helpers.php';
20+
agents_api_smoke_require_module();
21+
22+
$accepted = AgentsAPI\AI\Approvals\ApprovalDecision::accepted();
23+
$rejected = AgentsAPI\AI\Approvals\ApprovalDecision::from_string( AgentsAPI\AI\Approvals\ApprovalDecision::REJECTED );
24+
25+
agents_api_smoke_assert_equals( 'accepted', AgentsAPI\AI\Approvals\ApprovalDecision::ACCEPTED, 'accepted vocabulary is stable', $failures, $passes );
26+
agents_api_smoke_assert_equals( 'rejected', AgentsAPI\AI\Approvals\ApprovalDecision::REJECTED, 'rejected vocabulary is stable', $failures, $passes );
27+
agents_api_smoke_assert_equals( 'accepted', $accepted->value(), 'accepted decision exposes normalized value', $failures, $passes );
28+
agents_api_smoke_assert_equals( true, $accepted->is_accepted(), 'accepted decision reports accepted', $failures, $passes );
29+
agents_api_smoke_assert_equals( false, $accepted->is_rejected(), 'accepted decision does not report rejected', $failures, $passes );
30+
agents_api_smoke_assert_equals( 'rejected', (string) $rejected, 'rejected decision stringifies to normalized value', $failures, $passes );
31+
agents_api_smoke_assert_equals( true, $rejected->is_rejected(), 'rejected decision reports rejected', $failures, $passes );
32+
33+
$handler = new class() implements AgentsAPI\AI\Approvals\PendingActionHandlerInterface {
34+
public function handle_pending_action( AgentsAPI\AI\Approvals\ApprovalDecision $decision, array $apply_input, array $payload = array(), array $context = array() ): mixed {
35+
return array(
36+
'decision' => $decision->value(),
37+
'target' => $apply_input['target'] ?? null,
38+
'reason' => $payload['reason'] ?? null,
39+
'actor' => $context['actor'] ?? null,
40+
);
41+
}
42+
};
43+
44+
$handled = $handler->handle_pending_action(
45+
$accepted,
46+
array( 'target' => 'diff-123' ),
47+
array( 'reason' => 'looks-good' ),
48+
array( 'actor' => 'reviewer' )
49+
);
50+
51+
agents_api_smoke_assert_equals( 'accepted', $handled['decision'], 'handler receives decision object', $failures, $passes );
52+
agents_api_smoke_assert_equals( 'diff-123', $handled['target'], 'handler receives stored apply input', $failures, $passes );
53+
agents_api_smoke_assert_equals( 'looks-good', $handled['reason'], 'handler receives resolver payload', $failures, $passes );
54+
agents_api_smoke_assert_equals( 'reviewer', $handled['actor'], 'handler receives optional context', $failures, $passes );
55+
56+
$resolver = new class( $handler ) implements AgentsAPI\AI\Approvals\PendingActionResolverInterface {
57+
public function __construct( private AgentsAPI\AI\Approvals\PendingActionHandlerInterface $handler ) {}
58+
59+
public function resolve_pending_action( string $pending_action_id, AgentsAPI\AI\Approvals\ApprovalDecision $decision, array $payload = array(), array $context = array() ): mixed {
60+
return $this->handler->handle_pending_action(
61+
$decision,
62+
array( 'target' => $pending_action_id ),
63+
$payload,
64+
$context
65+
);
66+
}
67+
};
68+
69+
$resolved = $resolver->resolve_pending_action(
70+
'diff-456',
71+
AgentsAPI\AI\Approvals\ApprovalDecision::rejected(),
72+
array( 'reason' => 'needs-work' )
73+
);
74+
75+
agents_api_smoke_assert_equals( 'rejected', $resolved['decision'], 'resolver receives rejected decision', $failures, $passes );
76+
agents_api_smoke_assert_equals( 'diff-456', $resolved['target'], 'resolver maps pending action id to stored input', $failures, $passes );
77+
agents_api_smoke_assert_equals( 'needs-work', $resolved['reason'], 'resolver forwards payload to handler', $failures, $passes );
78+
79+
try {
80+
AgentsAPI\AI\Approvals\ApprovalDecision::from_string( 'approved' );
81+
agents_api_smoke_assert_equals( true, false, 'unknown decision is rejected', $failures, $passes );
82+
} catch ( InvalidArgumentException $e ) {
83+
agents_api_smoke_assert_equals( true, str_contains( $e->getMessage(), 'accepted or rejected' ), 'unknown decision is rejected', $failures, $passes );
84+
}
85+
86+
agents_api_smoke_finish( 'Agents API approval resolver contract', $failures, $passes );

0 commit comments

Comments
 (0)