@company-semantics/contracts 0.87.0 → 0.89.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.
- package/package.json +1 -1
- package/src/execution/__tests__/lifecycle.test.ts +222 -0
- package/src/execution/errors.ts +14 -0
- package/src/execution/expiry.ts +34 -0
- package/src/execution/index.ts +33 -0
- package/src/execution/lifecycle.ts +39 -0
- package/src/execution/status.ts +47 -0
- package/src/index.ts +11 -4
- package/src/mcp/{capability-graph.test.ts → __tests__/capability-graph.test.ts} +2 -2
- package/src/mcp/failure-context.ts +48 -0
- package/src/mcp/index.ts +5 -4
- package/src/message-parts/confirmation.ts +2 -0
- package/src/usage/types.ts +64 -0
package/package.json
CHANGED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { ExecutionState } from '../status.js'
|
|
3
|
+
import {
|
|
4
|
+
VALID_TRANSITIONS,
|
|
5
|
+
TERMINAL_STATES,
|
|
6
|
+
EFFECTIVE_TERMINAL_STATES,
|
|
7
|
+
assertValidTransition,
|
|
8
|
+
} from '../status.js'
|
|
9
|
+
import { LIFECYCLE_DECISIONS, applyIntent } from '../lifecycle.js'
|
|
10
|
+
import type { ExecutionErrorCode } from '../errors.js'
|
|
11
|
+
import { parseExpiresAt, isConfirmationExpired } from '../expiry.js'
|
|
12
|
+
|
|
13
|
+
const ALL_STATES: ExecutionState[] = [
|
|
14
|
+
'pending_confirmation',
|
|
15
|
+
'blocked_pending_approval',
|
|
16
|
+
'ready',
|
|
17
|
+
'executing',
|
|
18
|
+
'completed',
|
|
19
|
+
'completed_with_rollbacks',
|
|
20
|
+
'failed_retryable',
|
|
21
|
+
'failed_terminal',
|
|
22
|
+
'failed_with_partial_execution',
|
|
23
|
+
'cancelled',
|
|
24
|
+
'expired',
|
|
25
|
+
'undone',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// 1. EXHAUSTIVE TRANSITION MATRIX
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
describe('exhaustive transition matrix', () => {
|
|
32
|
+
for (const from of ALL_STATES) {
|
|
33
|
+
for (const to of ALL_STATES) {
|
|
34
|
+
const allowed = VALID_TRANSITIONS[from].includes(to)
|
|
35
|
+
it(`${from} -> ${to} should be ${allowed ? 'valid' : 'invalid'}`, () => {
|
|
36
|
+
if (allowed) {
|
|
37
|
+
expect(() => assertValidTransition(from, to)).not.toThrow()
|
|
38
|
+
} else {
|
|
39
|
+
expect(() => assertValidTransition(from, to)).toThrow(
|
|
40
|
+
`Invalid execution state transition: ${from} -> ${to}`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// 2. TERMINAL_STATES CONSISTENCY
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
describe('TERMINAL_STATES consistency', () => {
|
|
52
|
+
it('contains exactly the states with empty transition arrays', () => {
|
|
53
|
+
const expectedTerminal = ALL_STATES.filter(
|
|
54
|
+
(s) => VALID_TRANSITIONS[s].length === 0,
|
|
55
|
+
)
|
|
56
|
+
expect(expectedTerminal.length).toBeGreaterThan(0)
|
|
57
|
+
for (const state of expectedTerminal) {
|
|
58
|
+
expect(TERMINAL_STATES.has(state)).toBe(true)
|
|
59
|
+
}
|
|
60
|
+
expect(TERMINAL_STATES.size).toBe(expectedTerminal.length)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('does not contain states with non-empty transition arrays', () => {
|
|
64
|
+
const nonTerminal = ALL_STATES.filter(
|
|
65
|
+
(s) => VALID_TRANSITIONS[s].length > 0,
|
|
66
|
+
)
|
|
67
|
+
for (const state of nonTerminal) {
|
|
68
|
+
expect(TERMINAL_STATES.has(state)).toBe(false)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 3. EFFECTIVE_TERMINAL_STATES
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
describe('EFFECTIVE_TERMINAL_STATES', () => {
|
|
77
|
+
it('contains all TERMINAL_STATES', () => {
|
|
78
|
+
for (const state of TERMINAL_STATES) {
|
|
79
|
+
expect(EFFECTIVE_TERMINAL_STATES.has(state)).toBe(true)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('contains completed', () => {
|
|
84
|
+
expect(EFFECTIVE_TERMINAL_STATES.has('completed')).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('has size equal to TERMINAL_STATES + 1 (completed)', () => {
|
|
88
|
+
expect(EFFECTIVE_TERMINAL_STATES.size).toBe(TERMINAL_STATES.size + 1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('does not contain non-terminal, non-completed states', () => {
|
|
92
|
+
const nonEffectiveTerminal = ALL_STATES.filter(
|
|
93
|
+
(s) => !TERMINAL_STATES.has(s) && s !== 'completed',
|
|
94
|
+
)
|
|
95
|
+
for (const state of nonEffectiveTerminal) {
|
|
96
|
+
expect(EFFECTIVE_TERMINAL_STATES.has(state)).toBe(false)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// 4. DECISION TABLE KEY CONSISTENCY
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
describe('decision table key consistency', () => {
|
|
105
|
+
it('LIFECYCLE_DECISIONS.confirm keys match VALID_TRANSITIONS keys', () => {
|
|
106
|
+
const transitionKeys = Object.keys(VALID_TRANSITIONS).sort()
|
|
107
|
+
const decisionKeys = Object.keys(LIFECYCLE_DECISIONS.confirm).sort()
|
|
108
|
+
expect(decisionKeys).toEqual(transitionKeys)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// 5. APPROVE-TRANSITION DRIFT
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe('approve-transition drift', () => {
|
|
116
|
+
it('every state with approve decision can transition to ready', () => {
|
|
117
|
+
for (const state of ALL_STATES) {
|
|
118
|
+
if (LIFECYCLE_DECISIONS.confirm[state] === 'approve') {
|
|
119
|
+
expect(
|
|
120
|
+
VALID_TRANSITIONS[state].includes('ready'),
|
|
121
|
+
).toBe(true)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// 6. APPLY_INTENT UNIT TESTS
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
describe('applyIntent', () => {
|
|
131
|
+
it('pending_confirmation + confirm = approve', () => {
|
|
132
|
+
expect(applyIntent('pending_confirmation', 'confirm')).toBe('approve')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('expired + confirm = expired', () => {
|
|
136
|
+
expect(applyIntent('expired', 'confirm')).toBe('expired')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('cancelled + confirm = conflict', () => {
|
|
140
|
+
expect(applyIntent('cancelled', 'confirm')).toBe('conflict')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('ready + confirm = already_confirmed', () => {
|
|
144
|
+
expect(applyIntent('ready', 'confirm')).toBe('already_confirmed')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('blocked_pending_approval + confirm = awaiting_approval', () => {
|
|
148
|
+
expect(applyIntent('blocked_pending_approval', 'confirm')).toBe(
|
|
149
|
+
'awaiting_approval',
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('failed_terminal + confirm = conflict', () => {
|
|
154
|
+
expect(applyIntent('failed_terminal', 'confirm')).toBe('conflict')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('undone + confirm = conflict', () => {
|
|
158
|
+
expect(applyIntent('undone', 'confirm')).toBe('conflict')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// 7. EXECUTION_ERROR TYPE TESTS
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
describe('ExecutionErrorCode', () => {
|
|
166
|
+
it('covers all expected error codes', () => {
|
|
167
|
+
const codes: ExecutionErrorCode[] = [
|
|
168
|
+
'execution_expired',
|
|
169
|
+
'execution_already_confirmed',
|
|
170
|
+
'execution_awaiting_approval',
|
|
171
|
+
'execution_not_found',
|
|
172
|
+
'execution_forbidden',
|
|
173
|
+
'execution_invalid_transition',
|
|
174
|
+
]
|
|
175
|
+
expect(codes).toHaveLength(6)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// 8. EXPIRY HELPER TESTS
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
describe('parseExpiresAt', () => {
|
|
183
|
+
it('returns undefined for undefined input', () => {
|
|
184
|
+
expect(parseExpiresAt(undefined)).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('returns undefined for invalid date string', () => {
|
|
188
|
+
expect(parseExpiresAt('invalid')).toBeUndefined()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('returns correct epoch ms for valid ISO string', () => {
|
|
192
|
+
const result = parseExpiresAt('2026-01-01T00:00:00Z')
|
|
193
|
+
expect(result).toBe(new Date('2026-01-01T00:00:00Z').getTime())
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('returns undefined for empty string', () => {
|
|
197
|
+
expect(parseExpiresAt('')).toBeUndefined()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('isConfirmationExpired', () => {
|
|
202
|
+
it('returns false for undefined expiresAtMs', () => {
|
|
203
|
+
expect(isConfirmationExpired(undefined)).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('returns true for past timestamp', () => {
|
|
207
|
+
const pastMs = Date.now() - 60_000
|
|
208
|
+
expect(isConfirmationExpired(pastMs)).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('returns false for future timestamp', () => {
|
|
212
|
+
const futureMs = Date.now() + 60_000
|
|
213
|
+
expect(isConfirmationExpired(futureMs)).toBe(false)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('works with custom now parameter', () => {
|
|
217
|
+
const expiresAtMs = 1000
|
|
218
|
+
expect(isConfirmationExpired(expiresAtMs, 999)).toBe(false)
|
|
219
|
+
expect(isConfirmationExpired(expiresAtMs, 1000)).toBe(true)
|
|
220
|
+
expect(isConfirmationExpired(expiresAtMs, 1001)).toBe(true)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ExecutionErrorCode =
|
|
2
|
+
| 'execution_expired'
|
|
3
|
+
| 'execution_already_confirmed'
|
|
4
|
+
| 'execution_awaiting_approval'
|
|
5
|
+
| 'execution_not_found'
|
|
6
|
+
| 'execution_forbidden'
|
|
7
|
+
| 'execution_invalid_transition';
|
|
8
|
+
|
|
9
|
+
export interface ExecutionError {
|
|
10
|
+
readonly name: 'ExecutionError';
|
|
11
|
+
readonly code: ExecutionErrorCode;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly httpStatus?: number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized expiry check helpers for execution confirmations.
|
|
3
|
+
*
|
|
4
|
+
* Expiry logic lives here instead of inline in the confirm endpoint,
|
|
5
|
+
* the background job, and the client timer. Centralizing prevents drift.
|
|
6
|
+
*
|
|
7
|
+
* Uses pre-parsed epoch ms (number) instead of re-parsing ISO strings
|
|
8
|
+
* to avoid repeated Date construction.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses an ISO 8601 string to epoch ms.
|
|
13
|
+
* Returns undefined if the input is undefined or an invalid date (NaN guard).
|
|
14
|
+
* Called once at creation time; the result is compared cheaply thereafter.
|
|
15
|
+
*/
|
|
16
|
+
export function parseExpiresAt(
|
|
17
|
+
expiresAt: string | undefined,
|
|
18
|
+
): number | undefined {
|
|
19
|
+
if (expiresAt === undefined) return undefined
|
|
20
|
+
const ms = new Date(expiresAt).getTime()
|
|
21
|
+
return Number.isFinite(ms) ? ms : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compares pre-parsed epoch ms to determine if a confirmation has expired.
|
|
26
|
+
* Defaults `now` to Date.now(). Returns false if expiresAtMs is undefined.
|
|
27
|
+
*/
|
|
28
|
+
export function isConfirmationExpired(
|
|
29
|
+
expiresAtMs: number | undefined,
|
|
30
|
+
now?: number,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (expiresAtMs === undefined) return false
|
|
33
|
+
return (now ?? Date.now()) >= expiresAtMs
|
|
34
|
+
}
|
package/src/execution/index.ts
CHANGED
|
@@ -72,6 +72,39 @@ export type { ExecutionKind } from './kinds'
|
|
|
72
72
|
|
|
73
73
|
export type { ExecutionState } from './status'
|
|
74
74
|
|
|
75
|
+
export {
|
|
76
|
+
VALID_TRANSITIONS,
|
|
77
|
+
TERMINAL_STATES,
|
|
78
|
+
EFFECTIVE_TERMINAL_STATES,
|
|
79
|
+
assertValidTransition,
|
|
80
|
+
} from './status'
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Lifecycle Decision Engine
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
export type {
|
|
87
|
+
ExecutionIntent as LifecycleIntent,
|
|
88
|
+
LifecycleDecision,
|
|
89
|
+
} from './lifecycle'
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
LIFECYCLE_DECISIONS,
|
|
93
|
+
applyIntent,
|
|
94
|
+
} from './lifecycle'
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Expiry Helpers
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
export { parseExpiresAt, isConfirmationExpired } from './expiry'
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Error Types
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
export type { ExecutionErrorCode, ExecutionError } from './errors'
|
|
107
|
+
|
|
75
108
|
// =============================================================================
|
|
76
109
|
// Definition Types
|
|
77
110
|
// =============================================================================
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ExecutionState } from './status';
|
|
2
|
+
|
|
3
|
+
export type ExecutionIntent = 'confirm';
|
|
4
|
+
|
|
5
|
+
export type LifecycleDecision =
|
|
6
|
+
| 'approve'
|
|
7
|
+
| 'already_confirmed'
|
|
8
|
+
| 'awaiting_approval'
|
|
9
|
+
| 'expired'
|
|
10
|
+
| 'conflict';
|
|
11
|
+
|
|
12
|
+
type DecisionTable = Record<
|
|
13
|
+
ExecutionIntent,
|
|
14
|
+
Record<ExecutionState, LifecycleDecision>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export const LIFECYCLE_DECISIONS: DecisionTable = {
|
|
18
|
+
confirm: {
|
|
19
|
+
pending_confirmation: 'approve',
|
|
20
|
+
expired: 'expired',
|
|
21
|
+
cancelled: 'conflict',
|
|
22
|
+
blocked_pending_approval: 'awaiting_approval',
|
|
23
|
+
ready: 'already_confirmed',
|
|
24
|
+
executing: 'already_confirmed',
|
|
25
|
+
completed: 'already_confirmed',
|
|
26
|
+
completed_with_rollbacks: 'already_confirmed',
|
|
27
|
+
failed_retryable: 'conflict',
|
|
28
|
+
failed_terminal: 'conflict',
|
|
29
|
+
failed_with_partial_execution: 'conflict',
|
|
30
|
+
undone: 'conflict',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function applyIntent(
|
|
35
|
+
state: ExecutionState,
|
|
36
|
+
intent: ExecutionIntent,
|
|
37
|
+
): LifecycleDecision {
|
|
38
|
+
return LIFECYCLE_DECISIONS[intent][state];
|
|
39
|
+
}
|
package/src/execution/status.ts
CHANGED
|
@@ -41,6 +41,17 @@
|
|
|
41
41
|
* bigint), not timestamps. UNIQUE(execution_id, event_sequence) enforced
|
|
42
42
|
* at database level. event_sequence for ordering, created_at for duration —
|
|
43
43
|
* distinct concerns.
|
|
44
|
+
*
|
|
45
|
+
* ## Execution Invariants
|
|
46
|
+
*
|
|
47
|
+
* - **INV-EXEC-1**: Events are append-only. No UPDATE/DELETE on execution_events.
|
|
48
|
+
* - **INV-EXEC-2**: Current state = stateAfter of latest event (by event_sequence).
|
|
49
|
+
* execution_audit.state is a denormalized cache.
|
|
50
|
+
* - **INV-EXEC-3**: All transitions must satisfy VALID_TRANSITIONS[from].includes(to).
|
|
51
|
+
* - **INV-EXEC-4**: Transition validation + event insertion within same
|
|
52
|
+
* advisory-locked transaction.
|
|
53
|
+
* - **INV-EXEC-5**: failed_retryable -> ready loop bounded by executor retry budget.
|
|
54
|
+
* Retry events must include retryAttempt in metadata.
|
|
44
55
|
*/
|
|
45
56
|
export type ExecutionState =
|
|
46
57
|
| 'pending_confirmation'
|
|
@@ -55,3 +66,39 @@ export type ExecutionState =
|
|
|
55
66
|
| 'cancelled'
|
|
56
67
|
| 'expired'
|
|
57
68
|
| 'undone';
|
|
69
|
+
|
|
70
|
+
export const VALID_TRANSITIONS: Record<ExecutionState, readonly ExecutionState[]> = {
|
|
71
|
+
pending_confirmation: ['ready', 'blocked_pending_approval', 'cancelled', 'expired'],
|
|
72
|
+
blocked_pending_approval: ['ready', 'cancelled', 'expired'],
|
|
73
|
+
ready: ['executing'],
|
|
74
|
+
executing: [
|
|
75
|
+
'completed', 'completed_with_rollbacks',
|
|
76
|
+
'failed_retryable', 'failed_terminal',
|
|
77
|
+
'failed_with_partial_execution', 'cancelled',
|
|
78
|
+
],
|
|
79
|
+
completed: ['undone'],
|
|
80
|
+
completed_with_rollbacks: [],
|
|
81
|
+
failed_retryable: ['ready'],
|
|
82
|
+
failed_terminal: [],
|
|
83
|
+
failed_with_partial_execution: [],
|
|
84
|
+
cancelled: [],
|
|
85
|
+
expired: [],
|
|
86
|
+
undone: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const TERMINAL_STATES: ReadonlySet<ExecutionState> = new Set(
|
|
90
|
+
(Object.entries(VALID_TRANSITIONS) as [ExecutionState, readonly ExecutionState[]][])
|
|
91
|
+
.filter(([, targets]) => targets.length === 0)
|
|
92
|
+
.map(([state]) => state)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const EFFECTIVE_TERMINAL_STATES: ReadonlySet<ExecutionState> = new Set([
|
|
96
|
+
...TERMINAL_STATES,
|
|
97
|
+
'completed',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
export function assertValidTransition(from: ExecutionState, to: ExecutionState): void {
|
|
101
|
+
if (!VALID_TRANSITIONS[from].includes(to)) {
|
|
102
|
+
throw new Error(`Invalid execution state transition: ${from} -> ${to}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* @company-semantics/contracts
|
|
3
3
|
*
|
|
4
4
|
* Shared semantic vocabulary across Company Semantics codebases.
|
|
5
|
-
* Types
|
|
5
|
+
* Types and pure deterministic functions — no runtime code, no business logic.
|
|
6
6
|
*
|
|
7
|
-
* This package
|
|
8
|
-
*
|
|
9
|
-
* until they are proven stable.
|
|
7
|
+
* This package contains shared vocabulary and pure derivation functions.
|
|
8
|
+
* Structural types live in individual codebases until they are proven stable.
|
|
10
9
|
*
|
|
11
10
|
* @see https://github.com/company-semantics/company-semantics-contracts
|
|
12
11
|
*/
|
|
@@ -466,6 +465,14 @@ export type {
|
|
|
466
465
|
UsageByFeature,
|
|
467
466
|
UsageByUser,
|
|
468
467
|
OrgUsageResponse,
|
|
468
|
+
// Unified usage types (PRD-00276/277)
|
|
469
|
+
UnifiedUsageSummary,
|
|
470
|
+
UnifiedDailyUsage,
|
|
471
|
+
UnifiedProfileUsage,
|
|
472
|
+
UnifiedUserUsage,
|
|
473
|
+
UnifiedModelUsage,
|
|
474
|
+
UnifiedFeatureUsage,
|
|
475
|
+
UnifiedUsageResponse,
|
|
469
476
|
} from './usage/types'
|
|
470
477
|
|
|
471
478
|
// Runtime execution telemetry types
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { buildCapabilityGraph } from '
|
|
3
|
-
import type { MCPToolDescriptor } from '
|
|
2
|
+
import { buildCapabilityGraph } from '../capability-graph'
|
|
3
|
+
import type { MCPToolDescriptor } from '../index'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Helper to create minimal MCPToolDescriptor for testing.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FailureContext — Structured Recovery for MCP Tool Failures
|
|
3
|
+
*
|
|
4
|
+
* When an MCP tool fails, the system emits a FailureContext that constrains
|
|
5
|
+
* the agent's next action to registered recovery actions only.
|
|
6
|
+
*
|
|
7
|
+
* INVARIANTS:
|
|
8
|
+
* - recoveryActions is exhaustive — the LLM cannot invent alternatives
|
|
9
|
+
* - 'cancel' is always implicitly available (not declared in recoveryActions)
|
|
10
|
+
* - No UI strings — labels/hints are the app layer's responsibility
|
|
11
|
+
* - depth tracks recovery chain length (1 = first failure)
|
|
12
|
+
* - At most one FailureContext is active at a time (no stacking)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Structured recovery context emitted when an MCP tool call fails.
|
|
17
|
+
* Constrains the agent's next action to registered recovery options.
|
|
18
|
+
*/
|
|
19
|
+
export interface FailureContext {
|
|
20
|
+
/** MCP tool name that failed (e.g., 'cs_post_slack_message') */
|
|
21
|
+
tool: string;
|
|
22
|
+
/** Machine-readable error code from the error code registry */
|
|
23
|
+
reason: string;
|
|
24
|
+
/** Whether the same call might succeed on retry */
|
|
25
|
+
retryable: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Exhaustive list of allowed recovery actions.
|
|
28
|
+
* The agent MUST only invoke tools from this list until resolved.
|
|
29
|
+
* Empty array = cancel-only (no tool-based recovery available).
|
|
30
|
+
*/
|
|
31
|
+
recoveryActions: RecoveryAction[];
|
|
32
|
+
/**
|
|
33
|
+
* Recovery chain depth. Starts at 1 on first failure.
|
|
34
|
+
* Incremented when a recovery action itself fails.
|
|
35
|
+
* When depth exceeds MAX_RECOVERY_DEPTH, only pure (read-only) tools remain available.
|
|
36
|
+
*/
|
|
37
|
+
depth: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A recovery action the agent may take.
|
|
42
|
+
*
|
|
43
|
+
* Discriminated union — 'retry' is distinct from calling the same tool.
|
|
44
|
+
* Retry implies: arguments may change, timing may change, reason awareness.
|
|
45
|
+
*/
|
|
46
|
+
export type RecoveryAction =
|
|
47
|
+
| { type: 'retry' }
|
|
48
|
+
| { type: 'tool'; tool: string };
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResourceType } from './resources'
|
|
2
2
|
export type { ResourceType } from './resources'
|
|
3
3
|
export { buildCapabilityGraph } from './capability-graph'
|
|
4
|
+
export type { FailureContext, RecoveryAction } from './failure-context'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* MCP Tool Discovery Types
|
|
@@ -214,7 +215,7 @@ export interface MCPToolDescriptor {
|
|
|
214
215
|
* Edge in the capability graph connecting two tools via a resource.
|
|
215
216
|
* A tool that produces a resource connects to tools that consume it.
|
|
216
217
|
*
|
|
217
|
-
* @
|
|
218
|
+
* @see capability-graph.ts for buildCapabilityGraph() implementation
|
|
218
219
|
*/
|
|
219
220
|
export interface CapabilityGraphEdge {
|
|
220
221
|
/** Tool ID that produces the resource */
|
|
@@ -231,7 +232,7 @@ export interface CapabilityGraphEdge {
|
|
|
231
232
|
* Named workflow derived from capability graph paths.
|
|
232
233
|
* Represents a common multi-tool sequence (e.g., "Connect Slack → Ingest → Query").
|
|
233
234
|
*
|
|
234
|
-
* @
|
|
235
|
+
* @see capability-graph.ts for buildCapabilityGraph() implementation
|
|
235
236
|
*/
|
|
236
237
|
export interface ToolWorkflow {
|
|
237
238
|
/** Workflow name (e.g., "Slack Onboarding") */
|
|
@@ -246,7 +247,7 @@ export interface ToolWorkflow {
|
|
|
246
247
|
* Capability graph derived from tool resource flow metadata.
|
|
247
248
|
* Built from produces/consumes fields on MCPToolDescriptor.
|
|
248
249
|
*
|
|
249
|
-
* @
|
|
250
|
+
* @see capability-graph.ts for buildCapabilityGraph() implementation
|
|
250
251
|
*/
|
|
251
252
|
export interface CapabilityGraph {
|
|
252
253
|
/** Resource flow edges between tools */
|
|
@@ -269,7 +270,7 @@ export interface ToolDiscoveryResponse {
|
|
|
269
270
|
* Capability graph derived from tool resource flow metadata.
|
|
270
271
|
* Optional — discovery responses may or may not include the computed graph.
|
|
271
272
|
*
|
|
272
|
-
* @
|
|
273
|
+
* @see capability-graph.ts for buildCapabilityGraph() implementation
|
|
273
274
|
*/
|
|
274
275
|
graph?: CapabilityGraph
|
|
275
276
|
}
|
|
@@ -67,6 +67,8 @@ export interface ConfirmationData {
|
|
|
67
67
|
risk: ConfirmationRiskLevel;
|
|
68
68
|
/** Lifecycle record ID — must exist before confirmation part is emitted */
|
|
69
69
|
executionId: string;
|
|
70
|
+
/** ISO 8601 deadline for confirmation window. Absent for legacy messages. */
|
|
71
|
+
expiresAt?: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
package/src/usage/types.ts
CHANGED
|
@@ -46,6 +46,9 @@ export interface UsageByUser {
|
|
|
46
46
|
requestCount: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated Use UnifiedUsageResponse instead. Will be removed in v1.0.
|
|
51
|
+
*/
|
|
49
52
|
export interface OrgUsageResponse {
|
|
50
53
|
summary: UsageSummary;
|
|
51
54
|
daily: DailyUsage[];
|
|
@@ -53,3 +56,64 @@ export interface OrgUsageResponse {
|
|
|
53
56
|
byFeature: UsageByFeature[];
|
|
54
57
|
topUsers: UsageByUser[];
|
|
55
58
|
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Unified Usage Response (PRD-00276/277)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export interface UnifiedUsageSummary {
|
|
65
|
+
executions: number;
|
|
66
|
+
totalCostUsd: string;
|
|
67
|
+
totalTokens: number;
|
|
68
|
+
avgDurationMs: number;
|
|
69
|
+
failureRate: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface UnifiedDailyUsage {
|
|
73
|
+
date: string;
|
|
74
|
+
executions: number;
|
|
75
|
+
totalCostUsd: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UnifiedProfileUsage {
|
|
79
|
+
profile: string;
|
|
80
|
+
executions: number;
|
|
81
|
+
percentOfTotal: number;
|
|
82
|
+
totalCostUsd: string;
|
|
83
|
+
avgDurationMs: number;
|
|
84
|
+
failureRate: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UnifiedUserUsage {
|
|
88
|
+
userId: string;
|
|
89
|
+
email: string;
|
|
90
|
+
executions: number;
|
|
91
|
+
totalCostUsd: string;
|
|
92
|
+
totalTokens: number;
|
|
93
|
+
favoriteProfile: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UnifiedModelUsage {
|
|
97
|
+
model: string;
|
|
98
|
+
provider: string;
|
|
99
|
+
totalTokens: number;
|
|
100
|
+
estimatedCostUsd: string;
|
|
101
|
+
requestCount: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UnifiedFeatureUsage {
|
|
105
|
+
feature: string;
|
|
106
|
+
totalTokens: number;
|
|
107
|
+
estimatedCostUsd: string;
|
|
108
|
+
requestCount: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface UnifiedUsageResponse {
|
|
112
|
+
period: { start: string; end: string };
|
|
113
|
+
summary: UnifiedUsageSummary;
|
|
114
|
+
daily: UnifiedDailyUsage[];
|
|
115
|
+
byProfile: UnifiedProfileUsage[];
|
|
116
|
+
byUser: UnifiedUserUsage[];
|
|
117
|
+
byModel: UnifiedModelUsage[];
|
|
118
|
+
byFeature: UnifiedFeatureUsage[];
|
|
119
|
+
}
|