Skip to content

Commit 56f3b33

Browse files
feat: real Gemini 2.5 Pro inference + visible streaming
- Replace @google-cloud/vertexai SDK with direct REST + ADC auth. Earlier SDK calls returned empty text on Cloud Run; REST is robust across SDK versions and exposes the full response shape. - Surface provider (vertex/anthropic/stub) and model on every AgentTurn, rendered as a chip on each turn card so the demo never lies about who wrote the line. - Add SENTINEL_PACE_PHASE_MS / SENTINEL_PACE_TURN_MS so stub-mode runs stream phase-by-phase instead of landing in one paint. Real-LLM mode uses model latency as the natural pacer (no artificial sleeps). - Update Dockerfile for Next.js 16 standalone layout (now .next/standalone/sentinelcloud/web/server.js).
1 parent ff7ea73 commit 56f3b33

8 files changed

Lines changed: 160 additions & 267 deletions

File tree

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
2424
ENV PORT=8080
2525
ENV HOSTNAME=0.0.0.0
2626
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
27-
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
27+
# Next.js 16 standalone output places the runtime under
28+
# `.next/standalone/sentinelcloud/web/` when the build context is the repo root.
29+
# We flatten that here so the entrypoint stays `node server.js`.
30+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/sentinelcloud/web/ ./
2831
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
2932
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
3033
USER nextjs

web/components/RunStage.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,24 @@ function TurnCard({ turn }: { turn: any }) {
208208
critic: 'border-[#aab1c5]/30 text-[#aab1c5]',
209209
narrator: 'border-white/30 text-white',
210210
};
211+
const providerTone: Record<string, string> = {
212+
vertex: 'border-[#5b8cff]/40 text-[#5b8cff]',
213+
anthropic: 'border-[#fb923c]/40 text-[#fb923c]',
214+
stub: 'border-[var(--color-fg-3)]/40 text-[var(--color-fg-3)]',
215+
};
216+
const provider = turn.provider as string | undefined;
217+
const model = turn.model as string | undefined;
211218
return (
212219
<div className="glass p-3.5">
213-
<div className="flex items-center justify-between mb-1.5">
214-
<div className={`text-[11px] uppercase tracking-wider px-2 py-0.5 rounded border ${tone[role] || ''}`}>{role}</div>
220+
<div className="flex items-center justify-between mb-1.5 gap-2">
221+
<div className="flex items-center gap-1.5 flex-wrap">
222+
<div className={`text-[11px] uppercase tracking-wider px-2 py-0.5 rounded border ${tone[role] || ''}`}>{role}</div>
223+
{provider && (
224+
<div className={`text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded border ${providerTone[provider] || ''}`} title={model || provider}>
225+
{provider === 'vertex' ? 'gemini' : provider === 'anthropic' ? 'claude' : 'stub'}
226+
</div>
227+
)}
228+
</div>
215229
<div className="text-[11px] text-[var(--color-fg-3)] font-mono tabular-nums">
216230
conf {turn.confidence?.toFixed?.(2) ?? '-'} · {turn.latencyMs ?? 0}ms
217231
</div>

web/lib/agents/agents.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ function turn(partial: Partial<AgentTurn> & Pick<AgentTurn, 'agent' | 'runId'>):
2626
latencyMs: partial.latencyMs ?? 0,
2727
tokensIn: partial.tokensIn ?? 0,
2828
tokensOut: partial.tokensOut ?? 0,
29+
provider: partial.provider,
30+
model: partial.model,
2931
ts: partial.ts ?? Date.now(),
3032
};
3133
}
@@ -45,7 +47,7 @@ export async function runAnalyst(runId: string, signals: Signal[], topology: unk
4547
thought: j.thought ?? '',
4648
evidence: Array.isArray(j.evidence) ? j.evidence : [],
4749
confidence: clamp01(j.confidence),
48-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
50+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
4951
});
5052
}
5153

@@ -59,7 +61,7 @@ export async function runDevil(runId: string, signals: Signal[], analystThought:
5961
dissent: j.dissent ?? j.alternativeHypothesis ?? '',
6062
evidence: Array.isArray(j.evidenceForAlternative) ? j.evidenceForAlternative : [],
6163
confidence: clamp01(j.confidence),
62-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
64+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
6365
});
6466
}
6567

@@ -74,7 +76,7 @@ export async function runSafety(
7476
thought: j.thought ?? '',
7577
policyViolations: Array.isArray(j.policyViolations) ? j.policyViolations : [],
7678
confidence: clamp01(j.confidence),
77-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
79+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
7880
});
7981
}
8082

@@ -99,7 +101,7 @@ export async function runStrategist(
99101
proposal,
100102
costDeltaUsd: proposal?.estimatedCostUsdDelta,
101103
confidence: clamp01(j.confidence),
102-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
104+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
103105
});
104106
}
105107

@@ -111,7 +113,7 @@ export async function runVerifier(runId: string, proposal: Action, signals: Sign
111113
runId, agent: 'verifier',
112114
thought: `${j.thought ?? ''} | predicted=${JSON.stringify(j.predictedKpis ?? {})} | disagreement=${j.disagreementPct ?? '?'}%`,
113115
confidence: clamp01(j.confidence),
114-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
116+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
115117
});
116118
}
117119

@@ -124,7 +126,7 @@ export async function runCritic(runId: string, proposal: Action, toolCards: stri
124126
thought: j.thought ?? '',
125127
policyViolations: Array.isArray(j.violations) ? j.violations : [],
126128
confidence: clamp01(j.confidence),
127-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
129+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
128130
});
129131
}
130132

@@ -137,7 +139,7 @@ export async function runNarrator(runId: string, story: string): Promise<AgentTu
137139
runId, agent: 'narrator',
138140
thought: j.summary ?? '',
139141
confidence: clamp01(j.confidence ?? 0.8),
140-
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut,
142+
latencyMs: r.latencyMs, tokensIn: r.tokensIn, tokensOut: r.tokensOut, provider: r.provider, model: r.model,
141143
});
142144
}
143145

web/lib/agents/orchestrator.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,25 @@ export type RunEvent =
2929
| { type: 'done'; report: RunReport }
3030
| { type: 'error'; message: string };
3131

32+
// Pacing makes phase events visible to the human watching the screen.
33+
// Real LLM calls already take 2–10 s each; this is the floor so the stub
34+
// path also has breathing room. Disabled when a real provider is in use,
35+
// since model latency is the natural pacer.
36+
const PACE_PHASE_MS = Number(process.env.SENTINEL_PACE_PHASE_MS || 600);
37+
const PACE_TURN_MS = Number(process.env.SENTINEL_PACE_TURN_MS || 350);
38+
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
39+
3240
export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent, void, void> {
3341
const runId = `r_${nanoid(12)}`;
3442
const startedAt = Date.now();
3543
const turns: AgentTurn[] = [];
3644
const signals = [...scenario.signals].sort((a, b) => a.ts - b.ts);
45+
let stubInUse = false;
3746

3847
const emit = <T extends RunEvent>(e: T) => e;
48+
const phasePace = async () => { if (stubInUse && PACE_PHASE_MS > 0) await sleep(PACE_PHASE_MS); };
49+
const turnPace = async () => { if (stubInUse && PACE_TURN_MS > 0) await sleep(PACE_TURN_MS); };
50+
const trackProvider = (t: AgentTurn) => { if (t.provider === 'stub') stubInUse = true; };
3951

4052
try {
4153
yield emit({ type: 'phase', phase: 'ingest' });
@@ -45,16 +57,25 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
4557
yield emit({ type: 'memory', episodeIds: recalled.map(r => r.id) });
4658

4759
yield emit({ type: 'phase', phase: 'analyze' });
60+
await phasePace();
4861
const analyst = await runAnalyst(runId, signals, scenario.topology);
62+
trackProvider(analyst);
4963
turns.push(analyst); yield emit({ type: 'turn', turn: analyst });
64+
await turnPace();
5065

5166
yield emit({ type: 'phase', phase: 'debate' });
67+
await phasePace();
5268
const devil = await runDevil(runId, signals, analyst.thought);
69+
trackProvider(devil);
5370
turns.push(devil); yield emit({ type: 'turn', turn: devil });
71+
await turnPace();
5472

5573
yield emit({ type: 'phase', phase: 'strategize' });
74+
await phasePace();
5675
const strategist = await runStrategist(runId, signals, analyst.thought, devil.dissent || '');
76+
trackProvider(strategist);
5777
turns.push(strategist); yield emit({ type: 'turn', turn: strategist });
78+
await turnPace();
5879

5980
if (!strategist.proposal) {
6081
yield emit({ type: 'error', message: 'Strategist returned no proposal' });
@@ -66,16 +87,24 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
6687
const blastTurn: AgentTurn = { ...strategist, id: `t_${nanoid(10)}`, blastRadius: blast };
6788
turns.push(blastTurn);
6889
yield emit({ type: 'blast', score: blast });
90+
await turnPace();
6991

7092
yield emit({ type: 'phase', phase: 'verify' });
93+
await phasePace();
7194
const critic = await runCritic(runId, action, toolCardsAsText());
95+
trackProvider(critic);
7296
turns.push(critic); yield emit({ type: 'turn', turn: critic });
97+
await turnPace();
7398

7499
yield emit({ type: 'phase', phase: 'safety' });
100+
await phasePace();
75101
const safety = await runSafety(runId, action, constitutionToText(DEFAULT_CONSTITUTION));
102+
trackProvider(safety);
76103
turns.push(safety); yield emit({ type: 'turn', turn: safety });
104+
await turnPace();
77105

78106
yield emit({ type: 'phase', phase: 'policy_gate' });
107+
await phasePace();
79108
const det = checkDeterministic(action, DEFAULT_CONSTITUTION);
80109
const allViolations = [
81110
...det.violations,
@@ -84,6 +113,7 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
84113
];
85114
const policyAllowed = det.allowed && (safety.policyViolations || []).length === 0;
86115
yield emit({ type: 'policy', allowed: policyAllowed, violations: allViolations });
116+
await turnPace();
87117

88118
if (!policyAllowed) {
89119
action = {
@@ -98,31 +128,40 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
98128
}
99129

100130
const verifier = await runVerifier(runId, action, signals);
131+
trackProvider(verifier);
101132
turns.push(verifier); yield emit({ type: 'turn', turn: verifier });
133+
await turnPace();
102134

103135
yield emit({ type: 'phase', phase: 'confidence_gate' });
136+
await phasePace();
104137
const fused = fuseConfidence(turns);
105138
const gate = shouldAutoAct(action, fused, blast);
106139
yield emit({ type: 'gate', auto: gate.auto, threshold: gate.threshold, reason: gate.reason, fusedConfidence: fused });
140+
await turnPace();
107141

108142
yield emit({ type: 'action', action });
109143

110144
let outcome: RunReport['outcome'] = 'in_progress';
111145
let actuationOk = true;
112146
if (gate.auto && policyAllowed) {
113147
yield emit({ type: 'phase', phase: 'act' });
148+
await phasePace();
114149
const res = await actuate(action);
115150
actuationOk = res.ok;
116151
yield emit({ type: 'actuated', ok: res.ok, details: res.details, artifact: res.artifact });
117152
outcome = res.ok ? 'auto_resolved' : 'failed';
118153
} else {
119154
yield emit({ type: 'phase', phase: 'act' });
155+
await phasePace();
120156
yield emit({ type: 'actuated', ok: true, details: 'Paused for human review (HITL)' });
121157
outcome = 'hitl_required';
122158
}
159+
await turnPace();
123160

124161
yield emit({ type: 'phase', phase: 'verify_outcome' });
162+
await phasePace();
125163
yield emit({ type: 'phase', phase: 'learn' });
164+
await phasePace();
126165

127166
const finishedAt = Date.now();
128167
const mttrSec = Math.max(1, Math.round((finishedAt - startedAt) / 1000));

0 commit comments

Comments
 (0)