Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6c12bdfcb | ||
|
|
30a694358d | ||
|
|
e01dfa0cd7 | ||
|
|
6c4a2eb626 |
@@ -1,19 +0,0 @@
|
||||
import 'reflect-metadata';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CoordModule } from './coord.module.js';
|
||||
import { InteractionCoordinationService } from './interaction-coordination.service.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
|
||||
describe('CoordModule DI (compiled-metadata boot)', () => {
|
||||
it('resolves InteractionCoordinationService through Nest DI', async () => {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [CoordModule] })
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue({ canActivate: (): boolean => true })
|
||||
.compile();
|
||||
expect(moduleRef.get(InteractionCoordinationService)).toBeInstanceOf(
|
||||
InteractionCoordinationService,
|
||||
);
|
||||
await moduleRef.close();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
InteractionCoordinationClient,
|
||||
type CoordinationObservation,
|
||||
@@ -13,7 +13,6 @@ import type { CreateHandoffDto } from './interaction-coordination.dto.js';
|
||||
|
||||
export const COORDINATION_PORT = Symbol('COORDINATION_PORT');
|
||||
export const COORDINATION_CONFIG = Symbol('COORDINATION_CONFIG');
|
||||
export const HANDOFF_ID_FACTORY = Symbol('HANDOFF_ID_FACTORY');
|
||||
|
||||
const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000;
|
||||
const MAX_TRACKED_HANDOFFS = 1_000;
|
||||
@@ -61,8 +60,6 @@ export class InteractionCoordinationService {
|
||||
constructor(
|
||||
@Inject(COORDINATION_PORT) private readonly port: InteractionCoordinationPort,
|
||||
@Inject(COORDINATION_CONFIG) private readonly config: InteractionCoordinationConfig,
|
||||
@Optional()
|
||||
@Inject(HANDOFF_ID_FACTORY)
|
||||
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
|
||||
) {}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ overwritten on upgrade. (Layer model: `constitution/LAYER-MODEL.md`.)
|
||||
| TypeScript strict typing | `guides/TYPESCRIPT.md` |
|
||||
| QA / test strategy | `guides/QA-TESTING.md` |
|
||||
| Documentation (any code/API/auth/infra change) | `guides/DOCUMENTATION.md` |
|
||||
| Writing style (docs, comms, any prose) | `guides/WRITING-STYLE.md` |
|
||||
| Secrets / vault usage | `guides/VAULT-SECRETS.md` |
|
||||
| Tool/credential reference (service CLIs, wrappers) | `guides/TOOLS-REFERENCE.md` |
|
||||
| Memory protocol (OpenBrain capture/recall) | `guides/MEMORY.md` |
|
||||
|
||||
@@ -27,6 +27,14 @@ Master/slave model:
|
||||
- Do not perform destructive git/file actions without explicit instruction.
|
||||
- Browser automation (Playwright, Cypress, Puppeteer) MUST run in headless mode. Never launch a visible browser — it collides with the user's display and active session.
|
||||
|
||||
### Output standards (writing + code)
|
||||
|
||||
- Technical documentation follows **MOS-STE** (Mosaic Simplified Technical English — an adapted ASD-STE100 profile): short sentences, one instruction per sentence, active voice, one word per meaning, one term per concept. Full rules: `~/.config/mosaic/guides/WRITING-STYLE.md`.
|
||||
- Apply MOS-STE **hardest to verification artifacts** (acceptance criteria, witness predicates, gate/alarm conditions). There an ambiguous term produces a false green, not just a confused reader.
|
||||
- Source code follows the **Google Style Guide** for the language.
|
||||
- User-facing comms follow the user's declared `communicationStyle` in `USER.md` "Communication Preferences" (`direct` | `friendly` | `formal`, default `direct`); `guides/WRITING-STYLE.md` §5 maps each value to output. The documentation standard does not change with user preference.
|
||||
- **Carve-out:** MOS-STE does NOT apply to content that must carry a specific human voice (letters, personal or marketing prose, voice-matched output). A declared voice profile wins.
|
||||
|
||||
### Secrets handling (HARD RULE)
|
||||
|
||||
- Vault is the canonical source-of-truth for every secret in every environment. No exceptions.
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Writing Style Standard — MOS-STE (MANDATORY)
|
||||
|
||||
This guide defines how agents write. It sets one style standard per output type.
|
||||
It is written in the standard it defines, as a worked example.
|
||||
|
||||
**Adapted, not compliant.** MOS-STE (Mosaic Simplified Technical English) is an
|
||||
adapted profile of ASD-STE100. Mosaic does not license or certify against
|
||||
ASD-STE100. Mosaic uses the load-bearing rules and fits them to agent work. This
|
||||
is the same stance Mosaic takes toward DO-178B/C: use the rigor, do not claim the
|
||||
certification.
|
||||
|
||||
## Scope — which standard governs which output
|
||||
|
||||
| Output type | Standard |
|
||||
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| Technical documentation (READMEs, runbooks, PRDs, procedures, ADRs, guides, acceptance criteria, design docs) | **MOS-STE** (this guide) |
|
||||
| Source code and code comments | **Google Style Guide** for the language (§4) |
|
||||
| Inter-agent comms | MOS-STE by default (concise, structured) |
|
||||
| User-facing comms | **Per-user style choice** — read `USER.md` "Communication Preferences" (§5) |
|
||||
| End-user prose the user owns (marketing, letters, personal writing, voice-matched content) | The user's declared voice. MOS-STE does NOT apply. |
|
||||
|
||||
**The user-voice carve-out is absolute.** Do not apply MOS-STE to content that
|
||||
must carry a specific human voice (for example a cover letter, a personal
|
||||
message, or marketing copy). That content needs the user's voice. MOS-STE would
|
||||
damage it. When a project declares a voice profile, that profile wins.
|
||||
|
||||
## 1. Why one standard
|
||||
|
||||
Agent documentation drifts across projects. Different agents use different terms,
|
||||
sentence styles, and structures for the same concept. Readers lose time.
|
||||
Assumptions hide in ambiguous prose. One standard gives agents a clear target. It
|
||||
gives reviewers a clear test.
|
||||
|
||||
## 2. Where MOS-STE matters most — verification artifacts
|
||||
|
||||
Apply MOS-STE hardest to acceptance criteria, witness predicates, gate
|
||||
definitions, and alarm conditions. In prose, an ambiguous term produces a
|
||||
confused reader. In a verification artifact, an ambiguous term produces a false
|
||||
green — a check that passes without testing the claim.
|
||||
|
||||
The one-term-one-concept rule (rule 9) is the guard. When one word names two
|
||||
concepts in one predicate, the check can test the wrong concept and still pass.
|
||||
|
||||
**Worked failure.** A rename used a witness predicate with three clauses: ref A
|
||||
present, ref B absent, tip committed from this host. Every clause tested the git
|
||||
_ref_ (the channel). The claim under test was about a _field inside the payload_.
|
||||
The word "beacon" named two concepts in one sentence. Deleting ref B was the next
|
||||
scheduled step. That step flips the last clause green and certifies a state in
|
||||
which the payload still names the wrong host. The predicate was one planned action
|
||||
away from a false green on its normal path. The payload field was never tested.
|
||||
|
||||
Rule: when N failure modes share one observable, the observable is not a
|
||||
diagnostic. In a verification artifact, that ambiguity does not confuse a reader —
|
||||
it certifies the defect.
|
||||
|
||||
## 3. MOS-STE rules
|
||||
|
||||
### 3.1 Sentence rules
|
||||
|
||||
1. Keep sentences short. Use 20 words or fewer for a procedure. Use 25 words or
|
||||
fewer for a description. (Reasoning and doctrine prose relaxes this limit —
|
||||
see §3.4. A future lint enforces §3.1, not §3.4.)
|
||||
2. Write one instruction per sentence. In a procedure, give one command per step.
|
||||
3. Use the active voice. Write "Run the script." Do not write "The script should
|
||||
be run."
|
||||
4. Use the imperative for instructions. Start the sentence with the verb.
|
||||
5. Use simple verb tenses. Prefer the present tense. Avoid the perfect and
|
||||
progressive tenses when a simple tense works.
|
||||
6. Do not use an `-ing` form when it makes the meaning unclear.
|
||||
7. Write positive statements. State what to do, not only what to avoid.
|
||||
|
||||
### 3.2 Word rules
|
||||
|
||||
8. Use one word for one meaning. Do not use the same word in two senses.
|
||||
9. Use one term for one concept. Do not use synonyms for variety. Example: choose
|
||||
`secret`, `credential`, or `key` for each concept, and keep it.
|
||||
10. Use articles (`a`, `the`). Do not drop words to save space.
|
||||
11. Keep an approved-terms glossary per project. Add each domain noun and each
|
||||
chosen verb. Technical names (for example `Vault`, `cgroup`, `systemd`) are
|
||||
always allowed.
|
||||
12. Define an abbreviation at its first use. Then use it consistently.
|
||||
|
||||
### 3.3 Structure rules
|
||||
|
||||
13. Use a list for parallel items or sequential steps. Do not put them in one long
|
||||
sentence.
|
||||
14. Use a table for data with more than two dimensions.
|
||||
15. Use parallel structure in headings and steps.
|
||||
16. Repeat the noun. Do not use a pronoun when the reference is unclear.
|
||||
|
||||
### 3.4 Adaptation notes (where MOS-STE deviates from ASD-STE100, and why)
|
||||
|
||||
- **No licensed dictionary.** ASD-STE100 ships a controlled dictionary under
|
||||
copyright. MOS-STE uses per-project glossaries instead (rule 11).
|
||||
- **Domain terms are allowed.** MOS-STE keeps every term the work needs.
|
||||
- **Reasoning prose gets structure, not amputation.** Apply the sentence and word
|
||||
rules to design and doctrine writing. Allow the length a subtle argument needs.
|
||||
Readable-first beats rule-strict when the two conflict.
|
||||
|
||||
## 4. Code — Google Style Guide
|
||||
|
||||
Write source code to the Google Style Guide for the language (Python, TypeScript,
|
||||
Shell, Go, and so on). Match the existing file when a local convention already
|
||||
exists. Keep code comments to the MOS-STE sentence and word rules.
|
||||
|
||||
## 5. User-facing comms — a per-user choice
|
||||
|
||||
Mosaic is multi-user. Different users want different comms styles. The framework
|
||||
already carries the selectable setting: `communicationStyle` (`direct` |
|
||||
`friendly` | `formal`, default `direct`). `mosaic init` writes it, and the
|
||||
builder renders it into the generated `USER.md` "Communication Preferences"
|
||||
section. This guide adds the OUTPUT meaning of each value; do not invent new
|
||||
values.
|
||||
|
||||
The builder renders the style as prose bullets, not the token name, so match on
|
||||
the leading bullet the generated `USER.md` actually contains:
|
||||
|
||||
| `USER.md` leading bullet | Style | User-facing output |
|
||||
| ----------------------------- | ------------------ | ---------------------------------------------------------------------- |
|
||||
| "Direct and concise" | `direct` (default) | MOS-STE structure — short, active, defined terms, tables for overview. |
|
||||
| "Warm and conversational" | `friendly` | Warmer register. Full sentences, explain reasoning, fewer tables. |
|
||||
| "Professional and structured" | `formal` | Professional and structured. Thorough, with explicit recommendations. |
|
||||
|
||||
This setting governs **user-facing comms only**. It does not change the
|
||||
documentation standard (§3), which is always MOS-STE regardless of the value.
|
||||
|
||||
## 6. Enforcement
|
||||
|
||||
- **Now:** human review only. **No mechanical prose check exists today.** The
|
||||
pre-push gate runs typecheck, lint, build, and tests; it inspects no prose.
|
||||
Reviewers check output against the scope table and the MOS-STE rules by hand.
|
||||
- **Future:** an MOS-STE lint check (built from the §3.1 sentence rules) and a
|
||||
Google-style linter in the pre-push gate. A future linter enforces §3.1, not
|
||||
§3.4 — see the note at rule 1.
|
||||
@@ -1,317 +0,0 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest';
|
||||
import { HARNESS_CAPABILITIES, HARNESS_ERROR_CODES } from './index.js';
|
||||
import type {
|
||||
AttachConversation,
|
||||
ConversationSnapshot,
|
||||
CreateHarnessSession,
|
||||
HarnessAdapter,
|
||||
HarnessCapability,
|
||||
HarnessConversationService,
|
||||
HarnessDescriptor,
|
||||
HarnessError,
|
||||
HarnessErrorCode,
|
||||
HarnessEvent,
|
||||
HarnessEventEnvelope,
|
||||
HarnessInteractionState,
|
||||
HarnessPrompt,
|
||||
HarnessPromptReceipt,
|
||||
HarnessSelection,
|
||||
HarnessSessionHandle,
|
||||
HarnessSessionSnapshot,
|
||||
ResumeHarnessSession,
|
||||
SendHarnessTurn,
|
||||
TurnReceipt,
|
||||
} from '../index.js';
|
||||
|
||||
const EXPECTED_CAPABILITIES = [
|
||||
'modelSelection',
|
||||
'thinkingLevels',
|
||||
'images',
|
||||
'toolEvents',
|
||||
'extensionUi',
|
||||
'steering',
|
||||
'followUp',
|
||||
'compaction',
|
||||
'persistentResume',
|
||||
] as const satisfies readonly HarnessCapability[];
|
||||
|
||||
const EXPECTED_ERROR_CODES = [
|
||||
'auth_required',
|
||||
'selection_invalid',
|
||||
'catalog_unavailable',
|
||||
'catalog_stale',
|
||||
'model_unavailable',
|
||||
'no_viable_provider',
|
||||
'session_create_failed',
|
||||
'session_not_found',
|
||||
'resume_conflict',
|
||||
'session_busy',
|
||||
'auth_bundle_concurrency_unverified',
|
||||
'adapter_unavailable',
|
||||
'sandbox_unavailable',
|
||||
'rpc_version_unsupported',
|
||||
'rpc_protocol_error',
|
||||
'process_exited',
|
||||
'outcome_unknown',
|
||||
'interaction_unsupported',
|
||||
'aborted',
|
||||
] as const satisfies readonly HarnessErrorCode[];
|
||||
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unexpected contract variant: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
function describeEvent(event: HarnessEvent): string {
|
||||
switch (event.type) {
|
||||
case 'session.started':
|
||||
case 'session.state':
|
||||
case 'session.identity_changed':
|
||||
case 'turn.started':
|
||||
case 'text.delta':
|
||||
case 'thinking.delta':
|
||||
case 'tool.started':
|
||||
case 'tool.updated':
|
||||
case 'tool.finished':
|
||||
case 'interaction.required':
|
||||
case 'usage.updated':
|
||||
case 'turn.completed':
|
||||
case 'error':
|
||||
return event.type;
|
||||
default:
|
||||
return assertNever(event);
|
||||
}
|
||||
}
|
||||
|
||||
function describeError(error: HarnessError): HarnessErrorCode {
|
||||
switch (error.code) {
|
||||
case 'auth_required':
|
||||
case 'selection_invalid':
|
||||
case 'catalog_unavailable':
|
||||
case 'catalog_stale':
|
||||
case 'model_unavailable':
|
||||
case 'no_viable_provider':
|
||||
case 'session_create_failed':
|
||||
case 'session_not_found':
|
||||
case 'resume_conflict':
|
||||
case 'session_busy':
|
||||
case 'auth_bundle_concurrency_unverified':
|
||||
case 'adapter_unavailable':
|
||||
case 'sandbox_unavailable':
|
||||
case 'rpc_version_unsupported':
|
||||
case 'rpc_protocol_error':
|
||||
case 'process_exited':
|
||||
case 'outcome_unknown':
|
||||
case 'interaction_unsupported':
|
||||
case 'aborted':
|
||||
return error.code;
|
||||
default:
|
||||
return assertNever(error);
|
||||
}
|
||||
}
|
||||
|
||||
describe('generic harness contracts', (): void => {
|
||||
it('keeps harness, provider, model, conversation, native session, process, and seat separate', (): void => {
|
||||
const selection = {
|
||||
harnessId: 'pi',
|
||||
providerId: 'openai-codex',
|
||||
modelId: 'gpt-5-codex',
|
||||
} satisfies HarnessSelection;
|
||||
const snapshot = {
|
||||
conversationId: 'conversation-1',
|
||||
nativeSessionId: 'native-session-1',
|
||||
processId: 'process-1',
|
||||
seatId: 'seat-1',
|
||||
selection,
|
||||
state: 'idle',
|
||||
attachedClientIds: ['browser-1'],
|
||||
} satisfies HarnessSessionSnapshot;
|
||||
|
||||
const identifiers = [
|
||||
snapshot.selection.harnessId,
|
||||
snapshot.selection.providerId,
|
||||
snapshot.selection.modelId,
|
||||
snapshot.conversationId,
|
||||
snapshot.nativeSessionId,
|
||||
snapshot.processId,
|
||||
snapshot.seatId,
|
||||
];
|
||||
|
||||
expect(new Set(identifiers).size).toBe(7);
|
||||
expect(snapshot).toMatchObject({
|
||||
conversationId: 'conversation-1',
|
||||
nativeSessionId: 'native-session-1',
|
||||
processId: 'process-1',
|
||||
seatId: 'seat-1',
|
||||
selection,
|
||||
});
|
||||
});
|
||||
|
||||
it('advertises the complete capability set as checked literals', (): void => {
|
||||
const descriptor = {
|
||||
id: 'pi',
|
||||
displayName: 'Pi',
|
||||
capabilities: HARNESS_CAPABILITIES,
|
||||
} satisfies HarnessDescriptor;
|
||||
|
||||
expect(HARNESS_CAPABILITIES).toEqual(EXPECTED_CAPABILITIES);
|
||||
expect(descriptor.capabilities).toEqual(EXPECTED_CAPABILITIES);
|
||||
});
|
||||
|
||||
it('exposes every stable error code as an exhaustive discriminated union', (): void => {
|
||||
const selection: HarnessSelection = {
|
||||
harnessId: 'pi',
|
||||
providerId: 'openai-codex',
|
||||
modelId: 'gpt-5-codex',
|
||||
};
|
||||
const error: HarnessError = {
|
||||
code: 'model_unavailable',
|
||||
message: 'The selected model is unavailable.',
|
||||
retryable: true,
|
||||
correlationId: 'correlation-1',
|
||||
selection,
|
||||
};
|
||||
|
||||
expect(HARNESS_ERROR_CODES).toEqual(EXPECTED_ERROR_CODES);
|
||||
expect(describeError(error)).toBe('model_unavailable');
|
||||
const receipt = {
|
||||
conversationId: 'conversation-1',
|
||||
turnId: 'turn-1',
|
||||
correlationId: 'correlation-1',
|
||||
state: 'accepted',
|
||||
selection,
|
||||
} satisfies HarnessPromptReceipt;
|
||||
|
||||
expect(error.selection).toEqual(selection);
|
||||
expect(receipt.selection).toEqual(selection);
|
||||
expect('effectiveSelection' in error).toBe(false);
|
||||
expect('effectiveSelection' in receipt).toBe(false);
|
||||
});
|
||||
|
||||
it('wraps every normalized event variant in the persisted envelope', (): void => {
|
||||
const selection: HarnessSelection = {
|
||||
harnessId: 'pi',
|
||||
providerId: 'openai-codex',
|
||||
modelId: 'gpt-5-codex',
|
||||
};
|
||||
const toolStarted: HarnessEvent = {
|
||||
type: 'tool.started',
|
||||
toolCallId: 'tool-call-1',
|
||||
toolName: 'read',
|
||||
};
|
||||
const events: readonly HarnessEvent[] = [
|
||||
{ type: 'session.started', state: 'idle' },
|
||||
{ type: 'session.state', state: 'busy' },
|
||||
{
|
||||
type: 'session.identity_changed',
|
||||
identityGeneration: 2,
|
||||
label: 'Re-enrolled account',
|
||||
},
|
||||
{ type: 'turn.started' },
|
||||
{ type: 'text.delta', text: 'Hello' },
|
||||
{ type: 'thinking.delta', text: 'Reasoning' },
|
||||
toolStarted,
|
||||
{
|
||||
type: 'tool.updated',
|
||||
toolCallId: 'tool-call-1',
|
||||
toolName: 'read',
|
||||
message: 'Reading',
|
||||
},
|
||||
{
|
||||
type: 'tool.finished',
|
||||
toolCallId: 'tool-call-1',
|
||||
toolName: 'read',
|
||||
isError: false,
|
||||
},
|
||||
{
|
||||
type: 'interaction.required',
|
||||
requestId: 'interaction-1',
|
||||
interactionType: 'confirm',
|
||||
state: 'pending',
|
||||
prompt: 'Continue?',
|
||||
},
|
||||
{
|
||||
type: 'usage.updated',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
||||
},
|
||||
{ type: 'turn.completed', outcome: 'settled' },
|
||||
{
|
||||
type: 'error',
|
||||
error: {
|
||||
code: 'rpc_protocol_error',
|
||||
message: 'The harness protocol failed.',
|
||||
retryable: false,
|
||||
correlationId: 'correlation-1',
|
||||
selection,
|
||||
},
|
||||
},
|
||||
];
|
||||
const envelope: HarnessEventEnvelope = {
|
||||
conversationId: 'conversation-1',
|
||||
nativeSessionId: 'native-session-1',
|
||||
turnId: 'turn-1',
|
||||
correlationId: 'correlation-1',
|
||||
sequence: 42,
|
||||
nativeEntryCursor: 'native-entry-7',
|
||||
occurredAt: '2026-08-11T12:00:00.000Z',
|
||||
harnessId: 'pi',
|
||||
selection,
|
||||
event: toolStarted,
|
||||
};
|
||||
|
||||
expect(events.map(describeEvent)).toEqual([
|
||||
'session.started',
|
||||
'session.state',
|
||||
'session.identity_changed',
|
||||
'turn.started',
|
||||
'text.delta',
|
||||
'thinking.delta',
|
||||
'tool.started',
|
||||
'tool.updated',
|
||||
'tool.finished',
|
||||
'interaction.required',
|
||||
'usage.updated',
|
||||
'turn.completed',
|
||||
'error',
|
||||
]);
|
||||
expect(envelope).toMatchObject({
|
||||
conversationId: 'conversation-1',
|
||||
nativeSessionId: 'native-session-1',
|
||||
turnId: 'turn-1',
|
||||
correlationId: 'correlation-1',
|
||||
sequence: 42,
|
||||
nativeEntryCursor: 'native-entry-7',
|
||||
harnessId: 'pi',
|
||||
selection,
|
||||
event: toolStarted,
|
||||
});
|
||||
});
|
||||
|
||||
it('models the complete one-response interaction lifecycle', (): void => {
|
||||
const states = [
|
||||
'pending',
|
||||
'responded',
|
||||
'cancelled',
|
||||
'expired',
|
||||
] as const satisfies readonly HarnessInteractionState[];
|
||||
|
||||
expect(states).toEqual(['pending', 'responded', 'cancelled', 'expired']);
|
||||
});
|
||||
|
||||
it('preserves the approved adapter and conversation method signatures', (): void => {
|
||||
expectTypeOf<HarnessAdapter['create']>().toEqualTypeOf<
|
||||
(input: CreateHarnessSession) => Promise<HarnessSessionHandle>
|
||||
>();
|
||||
expectTypeOf<HarnessAdapter['resume']>().toEqualTypeOf<
|
||||
(input: ResumeHarnessSession) => Promise<HarnessSessionHandle>
|
||||
>();
|
||||
expectTypeOf<HarnessSessionHandle['prompt']>().toEqualTypeOf<
|
||||
(input: HarnessPrompt & { idempotencyKey: string }) => Promise<HarnessPromptReceipt>
|
||||
>();
|
||||
expectTypeOf<HarnessConversationService['attach']>().toEqualTypeOf<
|
||||
(input: AttachConversation & { afterSequence?: number }) => Promise<ConversationSnapshot>
|
||||
>();
|
||||
expectTypeOf<HarnessConversationService['send']>().toEqualTypeOf<
|
||||
(input: SendHarnessTurn & { idempotencyKey: string }) => Promise<TurnReceipt>
|
||||
>();
|
||||
});
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
import type { HarnessCapability, HarnessEvent, HarnessEventEnvelope } from './events.js';
|
||||
|
||||
/** Server-derived actor and seat authority. Browser input must not supply these values. */
|
||||
export interface HarnessActorContext {
|
||||
readonly actorId: string;
|
||||
readonly tenantId: string;
|
||||
readonly seatId: string;
|
||||
readonly correlationId: string;
|
||||
}
|
||||
|
||||
/** Exact harness/provider/model tuple. These concepts must never be merged into one identifier. */
|
||||
export interface HarnessSelection {
|
||||
readonly harnessId: string;
|
||||
readonly providerId: string;
|
||||
readonly modelId: string;
|
||||
}
|
||||
|
||||
export interface HarnessDescriptor {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly capabilities: readonly HarnessCapability[];
|
||||
}
|
||||
|
||||
export type HarnessInputType = 'text' | 'image';
|
||||
export type HarnessAuthState = 'ready' | 'auth_required' | 'unavailable';
|
||||
export type HarnessModelAvailability = 'available' | 'unavailable';
|
||||
|
||||
export interface HarnessCatalogEntry extends HarnessSelection {
|
||||
readonly displayName: string;
|
||||
readonly reasoningCapability: boolean;
|
||||
readonly thinkingLevels?: readonly string[];
|
||||
readonly inputTypes: readonly HarnessInputType[];
|
||||
readonly contextWindow?: number;
|
||||
readonly authState: HarnessAuthState;
|
||||
readonly availability: HarnessModelAvailability;
|
||||
}
|
||||
|
||||
export interface HarnessCatalog {
|
||||
readonly harnessId: string;
|
||||
readonly version: string;
|
||||
readonly fingerprint: string;
|
||||
readonly models: readonly HarnessCatalogEntry[];
|
||||
}
|
||||
|
||||
export interface CreateHarnessSession {
|
||||
readonly context: HarnessActorContext;
|
||||
readonly conversationId: string;
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
export interface ResumeHarnessSession {
|
||||
readonly context: HarnessActorContext;
|
||||
readonly conversationId: string;
|
||||
readonly nativeSessionId: string;
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
export type HarnessSessionState = 'starting' | 'idle' | 'busy' | 'evicted' | 'ended' | 'failed';
|
||||
|
||||
export interface HarnessSessionSnapshot {
|
||||
readonly conversationId: string;
|
||||
readonly nativeSessionId: string;
|
||||
/** Absent when the resumable native session has no active process. */
|
||||
readonly processId?: string;
|
||||
readonly seatId: string;
|
||||
readonly selection: HarnessSelection;
|
||||
readonly state: HarnessSessionState;
|
||||
readonly attachedClientIds: readonly string[];
|
||||
}
|
||||
|
||||
export interface AttachClient {
|
||||
readonly clientId: string;
|
||||
}
|
||||
|
||||
export interface HarnessPrompt {
|
||||
readonly turnId: string;
|
||||
readonly correlationId: string;
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export type HarnessTurnState =
|
||||
| 'prepared'
|
||||
| 'dispatching'
|
||||
| 'accepted'
|
||||
| 'streaming'
|
||||
| 'settled'
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
| 'interrupted'
|
||||
| 'outcome_unknown';
|
||||
|
||||
/** A successful receipt reports only the selected tuple; no substitute tuple is representable. */
|
||||
export interface HarnessPromptReceipt {
|
||||
readonly conversationId: string;
|
||||
readonly turnId: string;
|
||||
readonly correlationId: string;
|
||||
readonly state: HarnessTurnState;
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
export interface HarnessConfirmInteractionResponse {
|
||||
readonly requestId: string;
|
||||
readonly type: 'confirm';
|
||||
readonly accepted: boolean;
|
||||
}
|
||||
|
||||
export interface HarnessSelectInteractionResponse {
|
||||
readonly requestId: string;
|
||||
readonly type: 'select';
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface HarnessInputInteractionResponse {
|
||||
readonly requestId: string;
|
||||
readonly type: 'input';
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface HarnessEditorInteractionResponse {
|
||||
readonly requestId: string;
|
||||
readonly type: 'editor';
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface HarnessCancelInteractionResponse {
|
||||
readonly requestId: string;
|
||||
readonly type: 'cancel';
|
||||
}
|
||||
|
||||
export type HarnessInteractionResponse =
|
||||
| HarnessConfirmInteractionResponse
|
||||
| HarnessSelectInteractionResponse
|
||||
| HarnessInputInteractionResponse
|
||||
| HarnessEditorInteractionResponse
|
||||
| HarnessCancelInteractionResponse;
|
||||
|
||||
export type HarnessCloseReason =
|
||||
| 'client_request'
|
||||
| 'idle_timeout'
|
||||
| 'gateway_shutdown'
|
||||
| 'process_crash'
|
||||
| 'composition_changed'
|
||||
| 'session_ended';
|
||||
|
||||
export interface AttachConversation {
|
||||
readonly context: HarnessActorContext;
|
||||
readonly conversationId: string;
|
||||
readonly clientId: string;
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
export interface ConversationSnapshot {
|
||||
readonly session: HarnessSessionSnapshot;
|
||||
readonly lastSequence: number;
|
||||
/** Journal rows replayed after the caller's sequence, never best-effort socket history. */
|
||||
readonly replay: readonly HarnessEventEnvelope[];
|
||||
}
|
||||
|
||||
export interface DetachConversation {
|
||||
readonly context: HarnessActorContext;
|
||||
readonly conversationId: string;
|
||||
readonly clientId: string;
|
||||
}
|
||||
|
||||
export interface SendHarnessTurn extends HarnessPrompt {
|
||||
readonly context: HarnessActorContext;
|
||||
readonly conversationId: string;
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
export interface TurnReceipt extends HarnessPromptReceipt {}
|
||||
|
||||
export interface HarnessAdapter {
|
||||
readonly id: string;
|
||||
describe(context: HarnessActorContext): Promise<HarnessDescriptor>;
|
||||
catalog(context: HarnessActorContext): Promise<HarnessCatalog>;
|
||||
create(input: CreateHarnessSession): Promise<HarnessSessionHandle>;
|
||||
resume(input: ResumeHarnessSession): Promise<HarnessSessionHandle>;
|
||||
}
|
||||
|
||||
export interface HarnessSessionHandle {
|
||||
snapshot(): Promise<HarnessSessionSnapshot>;
|
||||
attach(input: AttachClient): Promise<void>;
|
||||
/** Removes a browser attachment; it does not terminate the process or native session. */
|
||||
detach(clientId: string): Promise<void>;
|
||||
prompt(input: HarnessPrompt & { idempotencyKey: string }): Promise<HarnessPromptReceipt>;
|
||||
setModel(selection: HarnessSelection): Promise<HarnessSelection>;
|
||||
abort(turnId: string): Promise<void>;
|
||||
respondInteraction(input: HarnessInteractionResponse): Promise<void>;
|
||||
events(listener: (event: HarnessEvent) => void): () => void;
|
||||
/** Stops the active process while retaining the resumable native session. */
|
||||
evictProcess(reason: HarnessCloseReason): Promise<void>;
|
||||
/** Explicitly and destructively ends the native session. */
|
||||
endSession(reason: HarnessCloseReason): Promise<void>;
|
||||
}
|
||||
|
||||
export interface HarnessConversationService {
|
||||
attach(input: AttachConversation & { afterSequence?: number }): Promise<ConversationSnapshot>;
|
||||
/** Removes only the browser attachment represented by the input. */
|
||||
detach(input: DetachConversation): Promise<void>;
|
||||
send(input: SendHarnessTurn & { idempotencyKey: string }): Promise<TurnReceipt>;
|
||||
/** Replays persisted Gateway journal rows after the supplied monotonic sequence. */
|
||||
subscribeFrom(conversationId: string, afterSequence: number): AsyncIterable<HarnessEventEnvelope>;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { HarnessSelection } from './contracts.js';
|
||||
|
||||
export const HARNESS_ERROR_CODES = [
|
||||
'auth_required',
|
||||
'selection_invalid',
|
||||
'catalog_unavailable',
|
||||
'catalog_stale',
|
||||
'model_unavailable',
|
||||
'no_viable_provider',
|
||||
'session_create_failed',
|
||||
'session_not_found',
|
||||
'resume_conflict',
|
||||
'session_busy',
|
||||
'auth_bundle_concurrency_unverified',
|
||||
'adapter_unavailable',
|
||||
'sandbox_unavailable',
|
||||
'rpc_version_unsupported',
|
||||
'rpc_protocol_error',
|
||||
'process_exited',
|
||||
'outcome_unknown',
|
||||
'interaction_unsupported',
|
||||
'aborted',
|
||||
] as const satisfies readonly string[];
|
||||
|
||||
export type HarnessErrorCode = (typeof HARNESS_ERROR_CODES)[number];
|
||||
|
||||
export interface HarnessErrorDto<Code extends HarnessErrorCode = HarnessErrorCode> {
|
||||
readonly code: Code;
|
||||
/** Safe for browser and operator-facing surfaces. */
|
||||
readonly message: string;
|
||||
readonly retryable: boolean;
|
||||
readonly correlationId: string;
|
||||
/** The requested selection; errors never report a substituted effective selection. */
|
||||
readonly selection: HarnessSelection;
|
||||
}
|
||||
|
||||
/** Closed discriminated union over every stable harness error code. */
|
||||
export type HarnessError = {
|
||||
readonly [Code in HarnessErrorCode]: HarnessErrorDto<Code>;
|
||||
}[HarnessErrorCode];
|
||||
@@ -1,139 +0,0 @@
|
||||
import type { HarnessError } from './errors.js';
|
||||
import type { HarnessSelection, HarnessSessionState } from './contracts.js';
|
||||
|
||||
export const HARNESS_CAPABILITIES = [
|
||||
'modelSelection',
|
||||
'thinkingLevels',
|
||||
'images',
|
||||
'toolEvents',
|
||||
'extensionUi',
|
||||
'steering',
|
||||
'followUp',
|
||||
'compaction',
|
||||
'persistentResume',
|
||||
] as const satisfies readonly string[];
|
||||
|
||||
export type HarnessCapability = (typeof HARNESS_CAPABILITIES)[number];
|
||||
|
||||
/** Durable lifecycle states; later persistence enforces one terminal response per request. */
|
||||
export type HarnessInteractionState = 'pending' | 'responded' | 'cancelled' | 'expired';
|
||||
export type HarnessInteractionType = 'confirm' | 'select' | 'input' | 'editor';
|
||||
|
||||
export interface HarnessUsage {
|
||||
readonly inputTokens: number;
|
||||
readonly outputTokens: number;
|
||||
readonly totalTokens: number;
|
||||
}
|
||||
|
||||
export type HarnessTurnOutcome =
|
||||
| 'settled'
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
| 'interrupted'
|
||||
| 'outcome_unknown';
|
||||
|
||||
export interface HarnessSessionStartedEvent {
|
||||
readonly type: 'session.started';
|
||||
readonly state: HarnessSessionState;
|
||||
}
|
||||
|
||||
export interface HarnessSessionStateEvent {
|
||||
readonly type: 'session.state';
|
||||
readonly state: HarnessSessionState;
|
||||
}
|
||||
|
||||
export interface HarnessSessionIdentityChangedEvent {
|
||||
readonly type: 'session.identity_changed';
|
||||
readonly identityGeneration: number;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface HarnessTurnStartedEvent {
|
||||
readonly type: 'turn.started';
|
||||
}
|
||||
|
||||
export interface HarnessTextDeltaEvent {
|
||||
readonly type: 'text.delta';
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export interface HarnessThinkingDeltaEvent {
|
||||
readonly type: 'thinking.delta';
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export interface HarnessToolStartedEvent {
|
||||
readonly type: 'tool.started';
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
}
|
||||
|
||||
export interface HarnessToolUpdatedEvent {
|
||||
readonly type: 'tool.updated';
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export interface HarnessToolFinishedEvent {
|
||||
readonly type: 'tool.finished';
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
readonly isError: boolean;
|
||||
}
|
||||
|
||||
export interface HarnessInteractionRequiredEvent {
|
||||
readonly type: 'interaction.required';
|
||||
readonly requestId: string;
|
||||
readonly interactionType: HarnessInteractionType;
|
||||
readonly state: HarnessInteractionState;
|
||||
readonly prompt: string;
|
||||
readonly options?: readonly string[];
|
||||
}
|
||||
|
||||
export interface HarnessUsageUpdatedEvent {
|
||||
readonly type: 'usage.updated';
|
||||
readonly usage: HarnessUsage;
|
||||
}
|
||||
|
||||
export interface HarnessTurnCompletedEvent {
|
||||
readonly type: 'turn.completed';
|
||||
readonly outcome: HarnessTurnOutcome;
|
||||
}
|
||||
|
||||
export interface HarnessErrorEvent {
|
||||
readonly type: 'error';
|
||||
readonly error: HarnessError;
|
||||
}
|
||||
|
||||
export type HarnessEvent =
|
||||
| HarnessSessionStartedEvent
|
||||
| HarnessSessionStateEvent
|
||||
| HarnessSessionIdentityChangedEvent
|
||||
| HarnessTurnStartedEvent
|
||||
| HarnessTextDeltaEvent
|
||||
| HarnessThinkingDeltaEvent
|
||||
| HarnessToolStartedEvent
|
||||
| HarnessToolUpdatedEvent
|
||||
| HarnessToolFinishedEvent
|
||||
| HarnessInteractionRequiredEvent
|
||||
| HarnessUsageUpdatedEvent
|
||||
| HarnessTurnCompletedEvent
|
||||
| HarnessErrorEvent;
|
||||
|
||||
/** Persisted normalized event plus Gateway-owned ordering and native reconciliation metadata. */
|
||||
export interface HarnessEventEnvelope {
|
||||
readonly conversationId: string;
|
||||
readonly nativeSessionId: string;
|
||||
readonly turnId?: string;
|
||||
readonly correlationId: string;
|
||||
/** Monotonic Gateway journal sequence within the conversation. */
|
||||
readonly sequence: number;
|
||||
/** Native session-entry cursor when the harness provides one. */
|
||||
readonly nativeEntryCursor?: string;
|
||||
readonly occurredAt: string;
|
||||
readonly harnessId: string;
|
||||
/** Exact effective selected provider/model tuple; no alternate success selection is exposed. */
|
||||
readonly selection: HarnessSelection;
|
||||
readonly event: HarnessEvent;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './contracts.js';
|
||||
export * from './events.js';
|
||||
export * from './errors.js';
|
||||
@@ -8,4 +8,3 @@ export * from './routing/index.js';
|
||||
export * from './commands/index.js';
|
||||
export * from './federation/index.js';
|
||||
export * from './reflection/index.js';
|
||||
export * from './harness/index.js';
|
||||
|
||||
Reference in New Issue
Block a user