Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/agent-conversations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Shared platform agent conversations

The additive 0.2.5 API discovers agents and exchanges messages through the existing
The additive 0.2.6 API discovers agents and exchanges messages through the existing
MainSequence `/mcp` gateway. No backend routes, schemas, account handling, or provider
configuration change. Claude UI, native draft/command handling and conversation controls
belong to `mainsequence-claude-desktop`; this package contains only the reusable
Expand All @@ -17,6 +17,7 @@ identity returned by discovery as `expectedAccount`.
| `agents-list` | `agent.list`, or semantic `agent.search` | Optional search, limit and offset |
| `agent-get` | `agent.get` | agentUid; optional expectedAccount |
| `agent-session` | `agent.get_or_create_session` | agentUid, expectedAccount; exactly one of handleId or sessionUid |
| `agent-readiness` | `agent.get_or_create_session`, `agent_session.resolve_runtime_access` | agentUid, sessionUid, expectedAccount; optional timeoutMs (1–630000) |
| `agent-send` | `a2a.send_message` | agentUid, expectedAccount, message, responseKind; exactly one of handleId, sessionUid, taskUid |
| `agent-task-get` | `a2a.get_task` | agentUid, expectedAccount, taskUid |
| `agent-task-wait` | `a2a.get_task`, `a2a.wait_task` | Same; optional afterSequence |
Expand All @@ -40,6 +41,24 @@ UID and handle. Agent, environment and returned identity are checked. Missing
provider/model defaults are reported without changing them. Client chat IDs are
external handles, not MainSequence parent-session foreign keys.

`agent-readiness` validates the saved session and waits for
`runtime_interaction.can_submit`, independently of deployment status or `is_ready`.
Transient states follow the backend retry interval (100 ms minimum) within one
630-second cap, shortened by an earlier backend wake deadline plus 30 seconds.
Cancellation interrupts the wait; terminal or malformed decisions fail closed.
Progress goes through the host's progress callback as fixed credential-free text.
The public result is only `{account, agentUid, sessionUid, ready: true}`. The raw
runtime access response (token, URL, diagnostics and other fields) is never exposed
or persisted. This can wake the deployed agent but sends no message or inference.
Readiness is not cached across operations or restored as permanent state.

Set `waitForReady: true` on `agent-send` to use the same readiness gate immediately
before dispatch, including task continuation. A readiness failure has no uncertain
message effect; a failure after dispatch retains the existing uncertain-send marker.
This option defaults off for compatibility with existing adapters, whose gateway
sender continues to own readiness. Claude 0.4.5 opts in. No UI package, Python SDK
or direct runtime HTTP transport is added.

A send may use that resolved `sessionUid`, or a stable handle for legacy clients.
The returned `sessionUid` identifies follow-ups. Message mode is synchronous. Task mode always requests `completion_policy:
poll`; `resume_caller` is for platform AgentSessions and is not used by desktop hosts.
Expand Down Expand Up @@ -79,3 +98,8 @@ platform tool deadlines. Claude 0.4.4 consumes this additive API. Release throug
the existing trusted-publisher workflow, verify the public archive integrity, and
then pin that archive in the consumer. Existing Codex marketplace and VS Code pins
are unchanged by this shared-library release.

Version 0.2.6 adds readiness preparation and opt-in send-time revalidation for Claude
0.4.5. Existing session allocation and other adapter behavior remain unchanged.

Agent list, search and detail results include `defaults: { provider, model, thinking }`, projected only from existing `llm_provider`, `llm_model` and `llm_thinking` fields. Empty or absent fields remain empty strings. These are Agent defaults for new sessions, not a claim about a resumed session’s resolved configuration. Runtime configuration and credential fields are excluded.
2 changes: 1 addition & 1 deletion manifests/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mainsequence-devtools",
"version": "0.2.5",
"version": "0.2.6",
"description": "Use MainSequence project MCPs from conversation and run explicit developer commands with the bundled runner.",
"author": {
"name": "MainSequence"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dev-mainsequence/devtools",
"version": "0.2.5",
"version": "0.2.6",
"description": "Shared MainSequence developer actions, project operations and deterministic Codex adapter",
"license": "MIT",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function createDevTools(options: ConstructorParameters<typeof MachineSess
options.signal?.throwIfAborted();
if (isAgentAction(action)) {
const backend = await resolveBackend(request.backend, undefined, env);
return safeResult(await new AgentClient(session, { signal: options.signal }).dispatch(request as AgentRequest, backend));
return safeResult(await new AgentClient(session, { signal: options.signal, progress: host.progress?.bind(host) }).dispatch(request as AgentRequest, backend));
}
if (action.startsWith('mcp-')) {
const backend = await resolveBackend(request.backend, process.cwd(), env);
Expand Down
66 changes: 66 additions & 0 deletions src/shared/agentReadiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ToolingError } from './models';

/** All runtime-access responses remain private. Only fixed, credential-free copy escapes. */
export async function waitForAgentReadiness(options: {
resolve(deadline: number): Promise<unknown>;
signal?: AbortSignal;
progress?: (message: string) => void;
timeoutMs?: number;
}): Promise<void> {
const budget = options.timeoutMs ?? 630000;
if (!Number.isFinite(budget) || budget < 1 || budget > 630000) {
throw new ToolingError('INVALID_REQUEST', 'Agent readiness timeout must be between 1 and 630000 milliseconds.');
}
let deadline = performance.now() + budget;
let lastProgress = '';
const progress = (message: string) => {
if (message !== lastProgress) { lastProgress = message; options.progress?.(message); }
};
const timedOut = () => new ToolingError('PROCESS_TIMEOUT', 'The agent did not become ready in time. No message was sent. Try again.');
progress('Checking agent…');
while (true) {
options.signal?.throwIfAborted();
if (performance.now() >= deadline) throw timedOut();
const access = await options.resolve(deadline) as any;
options.signal?.throwIfAborted();
if (performance.now() >= deadline) throw timedOut();
const interaction = access?.runtime_interaction;
if (!interaction || typeof interaction.can_submit !== 'boolean' || typeof interaction.state !== 'string') {
throw new ToolingError('API_ERROR', 'The platform did not provide a valid agent readiness decision. No message was sent.');
}
if (interaction.can_submit) {
if (!['ready', 'warning'].includes(interaction.state) || access.mode !== 'token'
|| typeof access.token !== 'string' || !access.token || typeof access.rpc_url !== 'string' || !access.rpc_url) {
throw new ToolingError('API_ERROR', 'The platform returned an incomplete agent readiness decision. No message was sent.');
}
progress('Agent ready');
return;
}
if (!['checking', 'starting', 'waking', 'updating'].includes(interaction.state)) {
const message = ({ update_required: 'The agent needs an update before it can accept messages.',
update_failed: 'The agent update failed.', unavailable: 'The agent is unavailable.' } as Record<string, string>)[interaction.state]
?? 'The agent cannot accept messages right now.';
throw new ToolingError('API_ERROR', `${message} No message was sent. Try again when it is available.`);
}
const retry = interaction.retry_after_ms;
if (typeof retry !== 'number' || !Number.isFinite(retry) || retry < 0) {
throw new ToolingError('API_ERROR', 'The platform did not provide a valid readiness retry interval. No message was sent.');
}
const wakeDeadline = access.runtime_presence?.wake?.deadline_at;
if (typeof wakeDeadline === 'string' && Number.isFinite(Date.parse(wakeDeadline))) {
deadline = Math.min(deadline, performance.now() + Math.max(0, Date.parse(wakeDeadline) + 30000 - Date.now()));
}
progress(interaction.state === 'updating' ? 'Agent update in progress…'
: interaction.state === 'checking' ? 'Checking agent…' : 'Starting agent…');
const delay = Math.max(100, retry), remaining = deadline - performance.now();
if (remaining <= 0) throw timedOut();
await new Promise<void>((resolve, reject) => {
const finish = () => { options.signal?.removeEventListener('abort', abort); resolve(); };
const timer = setTimeout(finish, Math.min(delay, remaining));
const abort = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); reject(new ToolingError('CANCELLED', 'Agent preparation cancelled. No message was sent.')); };
options.signal?.addEventListener('abort', abort, { once: true });
if (options.signal?.aborted) abort();
});
if (delay >= remaining) throw timedOut();
}
}
Loading
Loading