RcmEmailAutomation / sa-orchestration /includes /class-sa-actor-handoff.php
NujacsaintS's picture
reseed
f51c224
Raw
History Blame Contribute Delete
5.02 kB
<?php
defined( 'ABSPATH' ) || exit;
/**
* Actor Handoff Log β€” two-axis attribution (Board #23).
*
* Provides a clear, auditable boundary between AI-operated actor mode
* and human-operated actor mode WITHOUT mutating Fluent's own comment
* tables. Strictly additive: a small dedicated table records who
* "pressed send" for a given actor-authored comment.
*
* The two axes:
* created_by β€” the actor (Bridget, etc.). The displayed author. Unchanged.
* committed_by β€” the user who pressed send (a human user, or an agent
* service account user). Recorded here, queried via
* SA_Orch_Actor_Handoff::get_committer().
*
* Operator-mode detection β€” the full provenance chain works once Board #16
* (Token + Usage Visibility) ships its LLM token log. Until then, the
* token-presence check returns 'unknown' and detect_mode reflects that
* honestly:
*
* committer recorded + token row found β†’ 'ai'
* committer recorded + token row NOT found β†’ 'human'
* committer recorded + token state unknown β†’ 'unknown' (pre-#16; honest)
* committer NOT recorded β†’ 'unknown'
*
* The token-presence check is implemented as a filter:
*
* apply_filters( 'sa_orch_handoff_has_llm_token', null, $kind, $comment_id );
*
* Board #16 will register a callback that consults the token log. The
* default return of null means "unknown" β€” never a false positive.
*
* Doctrine constraints honored:
* - Actors remain non-binding; this layer is observational, not enforcing
* - No impact to Board progression rules
* - No schema refactors β€” strictly additive (one new table)
* - No Asterion writes
* - No automation
* - No UI surface beyond what already exists
*/
class SA_Orch_Actor_Handoff {
const KIND_FLUENT_BOARDS = 'fluent_boards';
const KIND_FLUENT_SUPPORT = 'fluent_support';
const MODE_AI = 'ai';
const MODE_HUMAN = 'human';
const MODE_UNKNOWN = 'unknown';
private static function table(): string {
global $wpdb;
return $wpdb->prefix . SA_ORCH_DB_PREFIX . 'actor_handoff';
}
/**
* Record (or update) the committer for an actor-authored comment.
* Idempotent on (kind, comment_id) β€” re-recording replaces the prior row.
*
* Returns true on success, false on invalid input or DB failure.
*/
public static function record_committer( string $kind, int $comment_id, int $committed_by_user_id ): bool {
if ( $comment_id <= 0 || $committed_by_user_id <= 0 ) {
return false;
}
$kind = sanitize_key( $kind );
if ( $kind === '' ) return false;
global $wpdb;
$tbl = self::table();
// Replace prior row, if any. Keeps the table thin.
$wpdb->delete( $tbl, [
'comment_kind' => $kind,
'comment_id' => $comment_id,
] );
$ok = $wpdb->insert( $tbl, [
'comment_kind' => $kind,
'comment_id' => $comment_id,
'committed_by_user_id' => $committed_by_user_id,
'recorded_at' => current_time( 'mysql', true ),
] );
return $ok !== false;
}
/**
* Get the recorded committer for a comment, or null if none recorded.
*/
public static function get_committer( string $kind, int $comment_id ): ?int {
if ( $comment_id <= 0 ) return null;
$kind = sanitize_key( $kind );
if ( $kind === '' ) return null;
global $wpdb;
$row = $wpdb->get_var( $wpdb->prepare(
"SELECT committed_by_user_id FROM " . self::table() . "
WHERE comment_kind = %s AND comment_id = %d
ORDER BY id DESC LIMIT 1",
$kind,
$comment_id
) );
return $row !== null ? (int) $row : null;
}
/**
* Detect operator mode for a comment. Returns one of the MODE_* constants.
* Never lies about uncertainty β€” returns 'unknown' rather than guessing.
*/
public static function detect_mode( string $kind, int $comment_id ): string {
$committer = self::get_committer( $kind, $comment_id );
if ( $committer === null ) {
return self::MODE_UNKNOWN;
}
/**
* Filter that signals whether an LLM token row exists for this commit.
* Returns: true (token row found β†’ AI mode),
* false (no token row β†’ human mode),
* null (unknown β€” token log not yet integrated, default).
*
* Board #16 will register a callback that consults the token log.
*/
$token_present = apply_filters(
'sa_orch_handoff_has_llm_token',
null,
$kind,
$comment_id
);
if ( $token_present === null ) return self::MODE_UNKNOWN;
if ( $token_present === true ) return self::MODE_AI;
return self::MODE_HUMAN;
}
}