@principles/host-runtime 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,16 @@ export interface EvaluatorRuntimeContextInput {
46
46
  export type EvaluatorRuntimeContextResolution = {
47
47
  readonly ok: true;
48
48
  readonly gateDeps: RefinerRuleHostGateDeps;
49
+ /**
50
+ * PRI-741: the registry snapshot the gateDeps were built from. Callers
51
+ * that need the host tool list (e.g. the host-alias parity projection)
52
+ * MUST derive it from HERE — a second independent
53
+ * `resolveWorkspaceHostToolSemantics` read could observe a different
54
+ * declaration snapshot than the replay gate.
55
+ */
56
+ readonly registry: ToolSemanticRegistry;
57
+ /** Host kind label(s) when known; the host-threaded path has none. */
58
+ readonly hostKinds?: readonly string[];
49
59
  } | {
50
60
  readonly ok: false;
51
61
  readonly reason: string;
@@ -47,6 +47,7 @@ export function createEvaluatorRuntimeContext(input) {
47
47
  if (input.toolSemantics) {
48
48
  return {
49
49
  ok: true,
50
+ registry: input.toolSemantics,
50
51
  gateDeps: createProductionGateDeps({ projectDir: input.workspaceDir, toolSemantics: input.toolSemantics }),
51
52
  };
52
53
  }
@@ -56,6 +57,8 @@ export function createEvaluatorRuntimeContext(input) {
56
57
  }
57
58
  return {
58
59
  ok: true,
60
+ registry: resolved.registry,
61
+ hostKinds: resolved.hostKinds,
59
62
  gateDeps: createProductionGateDeps({ projectDir: input.workspaceDir, toolSemantics: resolved.registry }),
60
63
  };
61
64
  }
@@ -32,6 +32,12 @@ export interface InternalizationConsumerCyclePorts {
32
32
  * hosts that have not declared semantics keep core-baseline behavior.
33
33
  */
34
34
  readonly toolSemantics?: ToolSemanticRegistry;
35
+ /**
36
+ * PRI-741: host kind label(s) for the artificer's HOST SEMANTIC CONTEXT
37
+ * block (e.g. ['openclaw']). Only meaningful together with `toolSemantics`;
38
+ * omitted → the artificer prompt is built without host context.
39
+ */
40
+ readonly hostKinds?: readonly string[];
35
41
  /** Log label used in human log prefixes ('AutoConsumer' | 'CodexWorker' | …). */
36
42
  readonly logLabel: string;
37
43
  readonly envGetter?: (name: string) => string | undefined;
@@ -21,7 +21,7 @@
21
21
  * env getter. Everything else is the existing production path.
22
22
  */
23
23
  import { createHash } from 'node:crypto';
24
- import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE,
24
+ import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE,
25
25
  // PRI-714: resolve outputLanguage from the same effective config the
26
26
  // runners already receive (EP-07: canonical resolved value, not raw input).
27
27
  resolveOutputLanguage, } from '@principles/core/runtime-v2';
@@ -144,6 +144,13 @@ async function runReconciliationBudget(workspaceDir, orchestrator, ports) {
144
144
  export async function runInternalizationConsumerCycle(workspaceDir, ports) {
145
145
  const { logger, emitEvent, logLabel, owner } = ports;
146
146
  const { hostToolCatalog, toolSemantics } = ports;
147
+ // PRI-741: artificer host semantic projection — built once from the SAME
148
+ // registry the activation gate/replay use (ports.toolSemantics), so the
149
+ // generation prompt teaches exactly the host dispatch surface that
150
+ // validateRuleReliability will later enforce. Sanitized for prompt use.
151
+ const artificerHostSemanticContext = toolSemantics !== undefined
152
+ ? buildArtificerHostSemanticContext(toolSemantics, ports.hostKinds ?? [])
153
+ : undefined;
147
154
  const envGetter = ports.envGetter ?? ((name) => process.env[name]);
148
155
  let orchestrator = null;
149
156
  const flag = loadFeatureFlagFromConfig(workspaceDir, INTERNALIZATION_AUTO_CONSUMER_FLAG_ID, {
@@ -240,13 +247,50 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
240
247
  // Advance the configured runner kinds in priority order (dreamer first,
241
248
  // then philosopher→…→rollout_reviewer under full-chain scope). Lease the
242
249
  // first ready task whose dependencies are satisfied.
250
+ //
251
+ // PRI-719 (EP002-R2 F4): the runtime profile is resolved PER KIND, before
252
+ // wakeOnce — `internalAgents.agents[kind].runtimeProfile` (fallback
253
+ // defaultRuntime) now actually governs the stage that runs on it. Before,
254
+ // the whole chain shared the diagnostician binding and every per-agent
255
+ // profile declaration was silently ignored. A kind whose profile cannot
256
+ // resolve is skipped observably (no lease taken for it).
243
257
  let wakeResult = null;
244
258
  let lastSkipDecision = 'no_ready_tasks';
245
259
  let lastSkipReason;
260
+ let taskRuntimeConfig = null;
246
261
  for (const kind of decision.runnerKinds) {
262
+ const agentName = AGENT_NAME_FOR_TASK_KIND[kind];
263
+ if (agentName === undefined) {
264
+ lastSkipDecision = 'runtime_config_error';
265
+ lastSkipReason = `no internal agent mapping for task kind '${kind}'`;
266
+ continue;
267
+ }
268
+ const kindRuntime = resolveRuntimeConfigForAgent(configResult.effective, agentName, {
269
+ getEnvVar: (name) => envGetter(name),
270
+ // Consumer stage scope is the internalization_full_chain flag, not
271
+ // agents[kind].enabled (the shipped default disables
272
+ // philosopher/evaluator/rolloutReviewer yet full-chain runs them) —
273
+ // see ResolveRuntimeConfigForAgentOptions.
274
+ ignoreAgentEnabled: true,
275
+ });
276
+ if (isRuntimeConfigError(kindRuntime)) {
277
+ // rc-9: the degradation is observable per kind; other kinds still run.
278
+ emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({
279
+ reason: 'runtime_config_error',
280
+ taskKind: kind,
281
+ agentName,
282
+ message: kindRuntime.message,
283
+ nextAction: kindRuntime.nextAction,
284
+ }));
285
+ logger.warn(`[PD:${logLabel}] Runtime config error for ${kind} (agent ${agentName}): ${kindRuntime.message}`);
286
+ lastSkipDecision = 'runtime_config_error';
287
+ lastSkipReason = kindRuntime.message;
288
+ continue;
289
+ }
247
290
  const candidate = await orchestrator.wakeOnce(kind);
248
291
  if (candidate.decision === 'would_lease') {
249
292
  wakeResult = candidate;
293
+ taskRuntimeConfig = kindRuntime;
250
294
  break;
251
295
  }
252
296
  // Keep decision/reason paired across iterations so the final SKIP log
@@ -254,7 +298,7 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
254
298
  lastSkipDecision = candidate.decision;
255
299
  lastSkipReason = candidate.decision === 'no_ready_tasks' ? candidate.reason : undefined;
256
300
  }
257
- if (!wakeResult) {
301
+ if (!wakeResult || taskRuntimeConfig === null) {
258
302
  // A: 预算由周期 finally 统一执行 (每周期恰一次,含 backlog 场景)
259
303
  const skipPayload = { decision: lastSkipDecision };
260
304
  if (lastSkipReason) {
@@ -264,8 +308,11 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
264
308
  logger.info(`[PD:${logLabel}] No task to consume: ${lastSkipDecision}`);
265
309
  return { ran: false, skipReason: lastSkipDecision };
266
310
  }
311
+ // PRI-719: everything downstream reads the LEASED TASK's resolved config —
312
+ // the kind-specific profile, not the shared gate resolution.
313
+ const taskRuntimeKind = taskRuntimeConfig.runtimeKind;
267
314
  let adapter;
268
- if (runtimeKind === 'pi-ai') {
315
+ if (taskRuntimeKind === 'pi-ai') {
269
316
  // PRI-419: when l2_dreamer flag is on AND this is a dreamer task, route
270
317
  // through the L2 multi-turn agent loop. Non-dreamer runners always use PiAi.
271
318
  const l2Flag = loadFeatureFlagFromConfig(workspaceDir, 'l2_dreamer');
@@ -275,14 +322,14 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
275
322
  logger: { warn: (msg) => logger.warn(msg) },
276
323
  });
277
324
  adapter = new L2AgentLoopAdapter({
278
- provider: runtimeConfigResult.provider ?? 'openai',
279
- model: runtimeConfigResult.model ?? 'gpt-4o',
280
- apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
281
- baseUrl: runtimeConfigResult.baseUrl,
325
+ provider: taskRuntimeConfig.provider ?? 'openai',
326
+ model: taskRuntimeConfig.model ?? 'gpt-4o',
327
+ apiKeyEnv: taskRuntimeConfig.apiKeyEnv ?? 'OPENAI_API_KEY',
328
+ baseUrl: taskRuntimeConfig.baseUrl,
282
329
  workspace: workspaceDir,
283
- totalBudgetMs: runtimeConfigResult.timeoutMs,
330
+ totalBudgetMs: taskRuntimeConfig.timeoutMs,
284
331
  // PRI-633: profile systemPrompt rides as the append layer.
285
- ...(runtimeConfigResult.systemPrompt ? { systemPrompt: runtimeConfigResult.systemPrompt } : {}),
332
+ ...(taskRuntimeConfig.systemPrompt ? { systemPrompt: taskRuntimeConfig.systemPrompt } : {}),
286
333
  }, {
287
334
  artifactReader: {
288
335
  // Explicit adapter: PIArtifactRecord → PdL2ArtifactReader. The store returns
@@ -301,27 +348,27 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
301
348
  }
302
349
  else {
303
350
  adapter = new PiAiRuntimeAdapter({
304
- provider: runtimeConfigResult.provider ?? 'openai',
305
- model: runtimeConfigResult.model ?? 'gpt-4o',
306
- apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
307
- maxRetries: runtimeConfigResult.maxRetries,
308
- maxTokens: runtimeConfigResult.maxTokens,
309
- timeoutMs: runtimeConfigResult.timeoutMs,
310
- baseUrl: runtimeConfigResult.baseUrl,
351
+ provider: taskRuntimeConfig.provider ?? 'openai',
352
+ model: taskRuntimeConfig.model ?? 'gpt-4o',
353
+ apiKeyEnv: taskRuntimeConfig.apiKeyEnv ?? 'OPENAI_API_KEY',
354
+ maxRetries: taskRuntimeConfig.maxRetries,
355
+ maxTokens: taskRuntimeConfig.maxTokens,
356
+ timeoutMs: taskRuntimeConfig.timeoutMs,
357
+ baseUrl: taskRuntimeConfig.baseUrl,
311
358
  workspace: workspaceDir,
312
359
  // PRI-633: profile systemPrompt rides as the append layer.
313
- ...(runtimeConfigResult.systemPrompt ? { systemPrompt: runtimeConfigResult.systemPrompt } : {}),
360
+ ...(taskRuntimeConfig.systemPrompt ? { systemPrompt: taskRuntimeConfig.systemPrompt } : {}),
314
361
  });
315
362
  }
316
363
  }
317
- else if (runtimeKind === 'openclaw-cli') {
364
+ else if (taskRuntimeKind === 'openclaw-cli') {
318
365
  adapter = new OpenClawCliRuntimeAdapter({
319
- runtimeMode: runtimeConfigResult.openclawMode ?? 'default',
366
+ runtimeMode: taskRuntimeConfig.openclawMode ?? 'default',
320
367
  workspaceDir: workspaceDir,
321
368
  });
322
369
  }
323
370
  else {
324
- throw new Error(`Unsupported runtime kind resolved for auto-consumer: ${runtimeKind}`);
371
+ throw new Error(`Unsupported runtime kind resolved for auto-consumer: ${taskRuntimeKind}`);
325
372
  }
326
373
  const { taskId } = wakeResult;
327
374
  const { taskKind } = wakeResult;
@@ -340,9 +387,9 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
340
387
  // same effective config (EP-07 canonical value).
341
388
  const runnerOptions = {
342
389
  owner,
343
- runtimeKind,
390
+ runtimeKind: taskRuntimeKind,
344
391
  effectiveConfig: configResult.effective,
345
- timeoutMs: runtimeConfigResult.timeoutMs,
392
+ timeoutMs: taskRuntimeConfig.timeoutMs,
346
393
  outputLanguage: resolveOutputLanguage(configResult.effective.config.principles?.outputLanguage).outputLanguage,
347
394
  };
348
395
  // PRI-634 A3: workspace-scoped telemetry sink for the evaluator runner.
@@ -369,7 +416,9 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
369
416
  runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator(), contentHashFn }, runnerOptions);
370
417
  break;
371
418
  case 'artificer':
372
- runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator(), contentHashFn }, runnerOptions);
419
+ runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator(), contentHashFn },
420
+ // PRI-741: thread the host semantic projection into generation.
421
+ { ...runnerOptions, ...(artificerHostSemanticContext !== undefined ? { hostSemanticContext: artificerHostSemanticContext } : {}) });
373
422
  break;
374
423
  case 'evaluator':
375
424
  // P0-D 生产接线: PRI-509 repair loop 正式进入 consumer (bounded,
@@ -401,6 +450,9 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
401
450
  ...(hostToolCatalog
402
451
  ? { hostToolCatalog: { readOnlyTools: [...hostToolCatalog.readOnlyTools], writeTools: [...hostToolCatalog.writeTools] } }
403
452
  : {}),
453
+ // PRI-741: host-name parity replay case from the SAME registry
454
+ // provenance as the gateDeps above.
455
+ ...(artificerHostSemanticContext !== undefined ? { hostSemanticContext: artificerHostSemanticContext } : {}),
404
456
  });
405
457
  break;
406
458
  case 'rollout_reviewer':
@@ -423,10 +475,18 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
423
475
  logger.warn(`[PD:${logLabel}] No consumer runner for task kind '${taskKind}'; skipping. Advance manually: pd runtime internalization run-once --runner ${taskKind}`);
424
476
  return { ran: false, skipReason: 'no_runner_for_kind', taskKind };
425
477
  }
426
- logger.info(`[PD:${logLabel}] Running ${taskKind} task: ${taskId}`);
478
+ // PRI-719: run evidence answers "which declared profile/provider/model
479
+ // actually executed this task" (declared == executed contract).
480
+ const runtimeEvidence = {
481
+ ...(taskRuntimeConfig.runtimeProfileId !== undefined ? { runtimeProfileId: taskRuntimeConfig.runtimeProfileId } : {}),
482
+ ...(taskRuntimeConfig.provider !== undefined ? { provider: taskRuntimeConfig.provider } : {}),
483
+ ...(taskRuntimeConfig.model !== undefined ? { model: taskRuntimeConfig.model } : {}),
484
+ };
485
+ logger.info(`[PD:${logLabel}] Running ${taskKind} task: ${taskId}${Object.keys(runtimeEvidence).length > 0 ? ` (runtime: ${JSON.stringify(runtimeEvidence)})` : ''}`);
427
486
  emitEvent('INTERNALIZATION_CONSUMER_RUN', JSON.stringify({
428
487
  taskId,
429
488
  taskKind,
489
+ ...runtimeEvidence,
430
490
  }));
431
491
  let runResult;
432
492
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.3.11",
3
+ "version": "0.4.0",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",