Files
GDevelop-Unlocked/packages/ai-studio/AiGeneration/Studio/NudgePolicy.js
T
BillyOutlast 30da3f8b4e Surgical conversion: Tier-1 patcher + launcher hook, Tier-2 extraction (Phases 1-5)
Replaces the 205-file fork delivery vehicle with patches/ + launcher/
+ versioned packages, per the implementation plan:

Phase 1: seam tripwires (27 tests) freezing every surgical target in
upstream (Usage predicate, %16 gate, SubscriptionChecker, debugger,
LoadingScreen branding, Electron hook surface, no-plugin-registry
verdict).

Phase 2: 5 generated Tier-1 hunks (round-trip verified; 3 files reach
byte-identical fork parity) + apply driver (dry-run gate, marker
verify, asar extract/repack).

Phase 3: launcher hook (preload fetch/XHR spoof of the two usage GETs,
BrowserWindow wrap, webRequest fallback, updater pin) + 06 loader
patch. Module._load interception dropped: renderers run a webpack
bundle, so the spoof rides the axios/XHR path the provider hydrates
from instead.

Phase 4: Tier-2 verbatim extraction (CustomAIClient + 12 Studio
modules + offline mock harness, manifest-pinned) + 07/08 verbatim
glue patches + pure generation shim with fail-closed leak guard.

Phase 5: web userscript demo (Tier-1 only, documented CORS/LAN/churn
limits). Phase 6 runbook + upstream provider-interface RFC draft
included; fork archiving left to owners (destructive).

Suite: 66 tests green (seams, patcher incl. re-apply refusal,
launcher incl. vm-sandbox fallback eval, tier-2 provenance, web).
2026-10-10 20:41:35 -04:00

96 lines
3.7 KiB
JavaScript

// @flow
import { type StudioPlanTask } from './PlanStore';
/**
* When the local studio's manager has stalled: it produced a plan, then ended
* its turn with an actionable task still undelegated (and no sub-agent running),
* so the multi-agent loop would sit idle forever under BYOK.
*
* This is the decision half of `useStudioNudge`, kept pure and dependency-light
* (`PlanStore`'s types only) so it can be unit-tested. The caller supplies the
* facts it already has: whether the request is the top-level manager, whether a
* turn is in flight, how many sub-agents are live and how many spawn calls are
* still unanswered.
*
* The nudge is deliberately conservative:
* - only the top-level manager (never a sub-agent);
* - never while a turn is in flight or work is pending;
* - never with a live or unanswered sub-agent;
* - never twice for the same plan state (the caller passes the signature it
* already nudged), so a stalled plan is nudged once, not every render;
* - a suspended/errored request is left alone (the user may have stopped it).
*/
export type StudioNudgeInput = {|
/** True for the top-level manager request (no parent, no studio role). */
isTopLevel: boolean,
status: string,
hasWorkInProgress: boolean,
isSending: boolean,
liveSubAgentCount: number,
pendingSubAgentCallCount: number,
/** Non-null only when an actionable (pending/in_progress) task exists. */
planSignature: string | null,
previouslyNudgedSignature: string | null,
/**
* True only for a request touched recently. Opening an old, abandoned chat
* must not resurrect its plan and spend tokens.
*/
isRecent: boolean,
|};
export const shouldNudgeStudioManager = (input: StudioNudgeInput): boolean => {
if (!input.isTopLevel) return false;
if (!input.isRecent) return false;
if (input.status === 'suspended' || input.status === 'error') return false;
if (input.isSending) return false;
if (input.hasWorkInProgress) return false;
if (input.liveSubAgentCount > 0) return false;
if (input.pendingSubAgentCallCount > 0) return false;
if (!input.planSignature) return false;
return input.planSignature !== input.previouslyNudgedSignature;
};
/**
* A stable signature of a plan's actionable state: the ids and statuses of the
* tasks that still need attention, in order. Null when there is no plan or
* nothing is actionable (every task is done or voided).
*
* `agentCallId` is included so a task that was delegated (but whose status has
* not flipped yet) is still distinguishable; a status-only signature would
* miss it only in the window before the spawn is stamped.
*/
export const getActionablePlanSignature = (
tasks: Array<StudioPlanTask> | null | void
): string | null => {
if (!Array.isArray(tasks) || tasks.length === 0) return null;
const actionable = tasks.filter(
task =>
task &&
typeof task === 'object' &&
(task.status === 'pending' || task.status === 'in_progress')
);
if (actionable.length === 0) return null;
return actionable
.map(task => `${task.id}:${task.status}:${task.agentCallId || ''}`)
.join('|');
};
/**
* The follow-up message the nudge sends to the stalled manager. Written as the
* user so it flows through the existing send path unchanged, and prefixed so
* the transcript makes clear it was the studio, not the person.
*/
export const buildStudioNudgeMessage = (
nextReadyTask: StudioPlanTask | null
): string => {
const taskHint = nextReadyTask
? ` Next ready task: ${nextReadyTask.id} (${nextReadyTask.title}).`
: '';
return (
'[Automatic reminder from the studio] Your plan still has a task that is ' +
`not delegated.${taskHint} Call spawn_agent for it now, or report plainly ` +
'why it cannot be done. Do not answer with prose only.'
);
};