Downgrade initial-user-turn CEO hardcode to semantic cue (C4 part 4/5)
Replace the 4-line `bash + node -e + fetch POST /api/companies/:id/
agents` + `POST /api/companies/:id/approvals` imperative with two
sentences that point at the Team Building Phase SOP and the new
`hire_agent` / `file_approval` tools (registered on the CEO in part
3/5). The cue is now `"This wake: your team is empty." +
"File 3-5 hire_agent requests and 1 approve_ceo_strategy via your
assigned tools. Execute now, do not write a plan document."`
All five pre-C4 couplings removed from this turn:
- Tool name (`bash`) — gone; cue references the tool surface generically
- Implementation detail (`node -e '...'` + `fetch`) — gone
- API paths (`/api/companies/:id/agents` + `/approvals`) — gone
- Business numbers (3-5 hires + 1 strategy) stay, moved into the cue
- Imperative tone (`NOW / Do not`) — dropped in favour of "execute now"
The fail-through path on the CEO lookup used to be `} catch {}`
(silent swallow). Migrated to `logAppError` under a recoverable
classification so a stale agent/company read still logs a structured
line instead of vanishing. The fallback behaviour (generic timer turn)
is unchanged — pi agent construction must not crash on a prompt-
assembly blip.
This commit is the first one to change model-observable behaviour.
The ceo-bootstrap-empty eval fixture (recorded against the old cue)
is expected to drift to 4/5 until re-recorded; IC / manager / non-CEO
fixtures stay green because they do not hit this code path.
tests: pnpm typecheck + pnpm test:unit (529) + pnpm test:eval (8/8) green
This commit is contained in:
1 parent
b839ce4f23
commit
003490e16b
1 file changed
+64
-29
@@ -19,6 +19,8 @@
|
|||||||
import type { AppDatabase } from "../database";
|
import type { AppDatabase } from "../database";
|
||||||
import type { AgentMessageRecord } from "@shared/types";
|
import type { AgentMessageRecord } from "@shared/types";
|
||||||
import { HUMAN_SENDER_ID } from "../messaging/dispatch";
|
import { HUMAN_SENDER_ID } from "../messaging/dispatch";
|
||||||
|
import { logAppError } from "../errors/index.js";
|
||||||
|
import log from "electron-log/main.js";
|
||||||
|
|
||||||
export interface InitialUserTurnDeps {
|
export interface InitialUserTurnDeps {
|
||||||
db: AppDatabase;
|
db: AppDatabase;
|
||||||
@@ -50,41 +52,74 @@ export function resolveInitialUserTurn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The generic timer turn hard-coded "deliverable reviews first,
|
// CEO / team-building semantic cue (C4 part 4/5).
|
||||||
// unblocking reports second, next step on assigned work third" —
|
//
|
||||||
// a ranking that presumes a manager or IC with tasks. For a new CEO
|
// The pre-C4 version of this block hard-coded the tool name (`bash`),
|
||||||
// in team_building mode, none of those clauses apply, so the model
|
// the runtime (`node -e ...`), the API paths (`/api/companies/:id/
|
||||||
// read "nothing to do" and returned without acting on the actual
|
// agents` + `/api/companies/:id/approvals`), the call count (3-5),
|
||||||
// Operating Instructions that explicitly told it to file hires.
|
// and the imperative tone ("Start the first POST NOW. Do not write
|
||||||
// Shortcut: when a CEO has zero active direct reports, replace the
|
// a plan document."). Every one of those was an implementation
|
||||||
// generic timer cue with a team-building cue that matches the CEO
|
// detail leaking into the LLM turn — when pi-agent-core swapped
|
||||||
// mode prompt.
|
// `bash`, when the HTTP path moved, or when the number policy shifted,
|
||||||
|
// the prompt silently drifted from reality and the CEO stopped filing
|
||||||
|
// hires.
|
||||||
|
//
|
||||||
|
// The new cue is deliberately short. The Team Building Phase
|
||||||
|
// operating instructions (see `ceo-modes/team-building.en.md`) own
|
||||||
|
// the SOP; this turn just points at it and tells the CEO to execute
|
||||||
|
// via the `hire_agent` and `file_approval` tools that C4 part 3/5
|
||||||
|
// registered on its agent. No tool names, no HTTP endpoints — the
|
||||||
|
// tool surface is the contract.
|
||||||
if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") {
|
if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") {
|
||||||
try {
|
const cue = tryResolveCeoTeamBuildingCue({ agentId, companyId, db: deps.db });
|
||||||
const agent = deps.db.entityCrud.getAgent(agentId);
|
if (cue !== null) return cue;
|
||||||
if (!agent.reportsTo) {
|
|
||||||
const activeReports = deps.db.agents
|
|
||||||
.getDirectReports(agentId, companyId)
|
|
||||||
.filter((r) => r.status !== "pending_approval" && r.status !== "terminated");
|
|
||||||
if (activeReports.length === 0) {
|
|
||||||
return [
|
|
||||||
"You are the CEO of this company. Your team is empty (zero active direct reports).",
|
|
||||||
"This wake has ONE job: call the `bash` tool to execute `node -e '...'` fetch POSTs that file 3–5 hire_agent requests against /api/companies/{companyId}/agents, then one approve_ceo_strategy POST against /api/companies/{companyId}/approvals.",
|
|
||||||
"The exact body shape and connectorId rules are in your Team Building Phase operating instructions above. Use a connectorId and model from the Available Connectors section (only ready connectors).",
|
|
||||||
"Start the first POST NOW. Do not write a plan document. Do not describe what you will do — execute the tool call.",
|
|
||||||
].join("\n\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Fall through to the generic turn — a lookup failure here must
|
|
||||||
// not crash the run; the system prompt still carries the full
|
|
||||||
// operating directives.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildGenericWakeTurn(trigger);
|
return buildGenericWakeTurn(trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the team-building cue when (and only when) the agent is a
|
||||||
|
* CEO (`reportsTo == null`) with zero active direct reports. Any other
|
||||||
|
* shape returns null and the caller falls through to
|
||||||
|
* `buildGenericWakeTurn`. A DB lookup failure is classified and
|
||||||
|
* logged — the caller never sees the raw error because the pi agent
|
||||||
|
* loop must not crash on a prompt-assembly hiccup, but the classified
|
||||||
|
* log line keeps the fail mode visible rather than swallowed.
|
||||||
|
*/
|
||||||
|
function tryResolveCeoTeamBuildingCue(params: {
|
||||||
|
agentId: string;
|
||||||
|
companyId: string;
|
||||||
|
db: AppDatabase;
|
||||||
|
}): string | null {
|
||||||
|
const { agentId, companyId, db } = params;
|
||||||
|
try {
|
||||||
|
const agent = db.entityCrud.getAgent(agentId);
|
||||||
|
if (agent.reportsTo) return null;
|
||||||
|
const activeReports = db.agents
|
||||||
|
.getDirectReports(agentId, companyId)
|
||||||
|
.filter((r) => r.status !== "pending_approval" && r.status !== "terminated");
|
||||||
|
if (activeReports.length > 0) return null;
|
||||||
|
return [
|
||||||
|
"This wake: your team is empty.",
|
||||||
|
"File 3-5 hire_agent requests and 1 approve_ceo_strategy via your assigned tools. Execute now, do not write a plan document.",
|
||||||
|
].join("\n\n");
|
||||||
|
} catch (error) {
|
||||||
|
logAppError(
|
||||||
|
log,
|
||||||
|
error,
|
||||||
|
{
|
||||||
|
kind: "recoverable",
|
||||||
|
code: "initial_user_turn.ceo_lookup.failed",
|
||||||
|
message: `CEO team-building cue lookup failed for agent ${agentId}; falling back to the generic timer turn.`,
|
||||||
|
context: { agentId, companyId },
|
||||||
|
},
|
||||||
|
"warn",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildCommunicationTurn(
|
function buildCommunicationTurn(
|
||||||
trigger: "message" | "peer_request",
|
trigger: "message" | "peer_request",
|
||||||
unread: AgentMessageRecord[],
|
unread: AgentMessageRecord[],
|
||||||
|
|||||||
Reference in new issue
Block a user