Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Standalone prompt enhancement

Enhance an unsent draft using a named saved LLM profile, without starting a
conversation. Availability and input failures can be verified without provider
credentials. Successful rewriting requires a compatible API-key profile.

Source: `openhands-agent-server/openhands/agent_server/prompt_enhancement_router.py`, `clients/typescript/src/client/prompt-enhancement-client.ts`

Routes: `GET /api/prompt-enhancement/availability/{name}`, `POST /api/prompt-enhancement/enhance`

## Sub-features

- `F35.auth`: both routes require the session key when authentication is enabled.
- `F35.availability`: an unknown profile reports unavailable with a stable code.
- `F35.missing-profile`: enhancement with an unknown profile returns 404.
- `F35.empty-input`: whitespace-only drafts return 400 before loading a profile.
- `F35.invalid-request`: wrong JSON field types return a sanitized 422 response.

## How to get to it (agent POV)

- REST: `GET /api/prompt-enhancement/availability/{name}` and
`POST /api/prompt-enhancement/enhance` with `profile_name` and `text`.
- Discovery: `/server_info` advertises `prompt_enhancement_v1`.
- TypeScript: `PromptEnhancementClient.checkAvailability()` and `.enhancePrompt()`;
requests accept an `AbortSignal`. The recipes below exercise REST.

## Driving it with control-agent-server

Preconditions:

- A baseline run is live, exported, and passes `doctor`; authentication is on.
- No profile named `qa-missing-enhancement` exists in the run's isolated store.
- No model credentials are required for these rejection-path recipes.

- **Authentication (`F35.auth`).** Omit the session key on both routes.
```sh
control-agent-server api GET /api/prompt-enhancement/availability/qa-missing-enhancement --auth none --expect 401 --save F35.auth/availability
control-agent-server api POST /api/prompt-enhancement/enhance --auth none --json '{"profile_name":"qa-missing-enhancement","text":"Draft."}' --expect 401 --save F35.auth/enhance
```
Both calls return 401 before accessing the profile or model.
- **Availability (`F35.availability`).** Query an absent profile with valid auth.
```sh
control-agent-server api GET /api/prompt-enhancement/availability/qa-missing-enhancement --expect 200 --check available eq false --check code eq profile_not_found --save F35.availability/missing
```
The profile is unavailable with code `profile_not_found`.
- **Missing profile (`F35.missing-profile`).** Submit a nonempty draft.
```sh
control-agent-server api POST /api/prompt-enhancement/enhance --json '{"profile_name":"qa-missing-enhancement","text":"Draft."}' --expect 404 --check code eq profile_not_found --save F35.missing-profile/missing
```
The request fails with code `profile_not_found`.
- **Empty draft (`F35.empty-input`).** Submit only whitespace.
```sh
control-agent-server api POST /api/prompt-enhancement/enhance --json '{"profile_name":"qa-missing-enhancement","text":" "}' --expect 400 --check code eq empty_input --save F35.empty-input/empty
```
The input error takes precedence over the absent profile.
- **Invalid request (`F35.invalid-request`).** Submit a non-string draft.
```sh
control-agent-server api POST /api/prompt-enhancement/enhance --json '{"profile_name":"qa-missing-enhancement","text":{"private":"QA_PRIVATE_INPUT"}}' --expect 422 --check detail.0.type eq string_type --check detail.0.input missing --check detail.0.ctx missing --save F35.invalid-request/type
```
The validation response retains the error type but omits raw input and context.

## Gotchas

- These recipes do not prove real-model rewriting or the TypeScript entry points.
A model-backed check needs a compatible saved API-key profile, disabled payload
tracing/logging, and credentials. Preserve intent, language, constraints, code,
paths, URLs and numbers when evaluating the result; a mock response cannot
establish this quality.
- Profiles with custom provider body options or subscription authentication are
unavailable because the endpoint must bound output and disable tools/storage.
- Input and output character limits and the server timeout bound the request.
Client cancellation stops waiting but may not cancel provider generation.
- There are no conversation, event, tool, or settings mutations in this API.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ recipe.

Maintenance baseline: main@8ba966d1d3af49f08e04349c055009e1614b48d6 (2026-10-07). The first pass; the next maintenance pass starts from this commit, a pass proposes the next baseline in its PR, and merging that PR accepts it.

The map has **950 sub-features**.
The map has **955 sub-features**.

## Baseline preconditions

Expand Down Expand Up @@ -99,6 +99,7 @@ them honest: [../maintenance.md](../maintenance.md).
| F32 | [Event webhooks and product telemetry](F32-webhooks-and-telemetry.md) | event webhooks (event batches, conversation updates, headers, buffering, flush, limits, retries) and product telemetry (off by default, exporter lifecycle, sanitized payloads, consent) | behavior only: `webhooks` / `OH_WEBHOOKS_*`, `OH_TELEMETRY_*` | llm; launch `--webhook-sink` | 30 |
| F33 | [Python SDK remote client (RemoteConversation, RemoteWorkspace)](F33-sdk-remote-client.md) | the Python SDK in this checkout against the server: RemoteWorkspace commands, files, git and settings readers; RemoteConversation create, run, callbacks, streaming, reattach, fork, confirmation, pause, interrupt, stop hooks, client tools, completion logs | behavior only: `Workspace(host=...)`, `RemoteWorkspace`, `RemoteConversation` | llm, tmux, git | 43 |
| F34 | [TypeScript client (@openhands/typescript-client)](F34-typescript-client.md) | the TypeScript client against this checkout: server and version gate, workspace, bash, conversation, state, events and socket clients, hooks and MCP clients, endpoint audit and drift | behavior only: `@openhands/typescript-client` (`clients/typescript`) | llm, node, uvx, git, tmux, network | 32 |
| F35 | [Standalone prompt enhancement](F35-prompt-enhancement.md) | availability, authentication and input/profile rejection paths | `GET /api/prompt-enhancement/availability/{name}`, `POST /api/prompt-enhancement/enhance`; TypeScript `PromptEnhancementClient` | baseline | 5 |

## Not mapped

Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/typescript-client-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install uv and Python 3.13
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
python-version: '3.13'

- name: Test Agent Server contract tooling
run: npm run test:agent-server-api-tooling

Expand Down Expand Up @@ -57,6 +63,9 @@ jobs:
- name: Check generated Agent Server contract
run: npm run check:agent-server-api

- name: Check generated prompt enhancement contract
run: npm run check:prompt-enhancement-api

public-type-budget:
runs-on: ubuntu-latest

Expand Down
35 changes: 35 additions & 0 deletions clients/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,41 @@ await conversation.updateSecrets({

## API Reference

### Standalone prompt enhancement

Use `PromptEnhancementClient` to improve a draft before creating a conversation.
The server resolves the saved profile's credentials and does not run an agent or
tools. Call `checkAvailability` before presenting the feature; it verifies the
server capability and whether the selected profile resolves, but does not test
provider availability.

```typescript
const prompts = new PromptEnhancementClient({
host: 'http://localhost:3000',
apiKey: 'your-session-api-key',
});
const availability = await prompts.checkAvailability('default');

if (availability.available) {
const controller = new AbortController();
const result = await prompts.enhancePrompt(
{ profile_name: 'default', text: 'Find the cause of this error.' },
{ signal: controller.signal }
);
console.log(result.enhanced_text);
}
```

Aborting rejects the client request promptly. A disconnected client may not
cancel the provider request; the Agent Server enforces its own timeout.
Prompt enhancement is unavailable while LLM tracing or payload logging is enabled.
Profiles with custom provider body options or subscription authentication are also
unavailable: they cannot guarantee the no-tools, no-storage and output-token
boundaries. Generation uses at most 8,192 output tokens, retaining a smaller
profile limit when configured.
Context-only OpenTelemetry instrumentation can still record private text in
provider exceptions, so it is also rejected.

### Conversation

Factory function that creates conversations with OpenHands agents.
Expand Down
2 changes: 2 additions & 0 deletions clients/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"build": "tsc && node ./scripts/copy-json-assets.mjs && node ./scripts/rewrite-relative-imports.mjs",
"generate:agent-server-api": "node ./scripts/generate-agent-server-api.mjs",
"check:agent-server-api": "node ./scripts/check-agent-server-api.mjs",
"generate:prompt-enhancement-api": "node ./scripts/prompt-enhancement-api-schema.mjs --write",
"check:prompt-enhancement-api": "node ./scripts/prompt-enhancement-api-schema.mjs",
"summarize:agent-server-api": "node ./scripts/summarize-agent-server-api.mjs",
"test:agent-server-api-tooling": "node ./scripts/test-agent-server-api-tooling.mjs",
"test:endpoint-audit-tooling": "node ./scripts/test-endpoint-audit.mjs",
Expand Down
76 changes: 69 additions & 7 deletions clients/typescript/scripts/generate-agent-server-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,63 @@ const generatedFile = resolve(
);
const explicitSchemaPath = process.env.AGENT_SERVER_OPENAPI_PATH;
const explicitSchemaSource = process.env.AGENT_SERVER_SCHEMA_SOURCE;
const pathPrefix = process.env.AGENT_SERVER_OPENAPI_PATH_PREFIX;

function selectPathPrefix(schema, prefix) {
const paths = Object.fromEntries(
Object.entries(schema.paths ?? {}).filter(([path]) => path.startsWith(prefix))
);
if (Object.keys(paths).length === 0) {
throw new Error(`No OpenAPI paths start with ${prefix}`);
}

const schemas = schema.components?.schemas ?? {};
const pending = new Set();
const selected = {};
const addReferences = (value) => {
if (Array.isArray(value)) {
value.forEach(addReferences);
} else if (value && typeof value === 'object') {
const reference = value.$ref;
const schemaPrefix = '#/components/schemas/';
if (typeof reference === 'string' && reference.startsWith(schemaPrefix)) {
pending.add(
reference.slice(schemaPrefix.length).replaceAll('~1', '/').replaceAll('~0', '~')
);
}
Object.values(value).forEach(addReferences);
}
};

addReferences(paths);
while (pending.size > 0) {
const name = pending.values().next().value;
pending.delete(name);
if (Object.hasOwn(selected, name)) continue;
if (!Object.hasOwn(schemas, name)) {
throw new Error(`OpenAPI schema reference not found: ${name}`);
}
selected[name] = schemas[name];
addReferences(schemas[name]);
}

return {
...schema,
paths,
components: { ...schema.components, schemas: selected },
};
}

async function main() {
const { schema, source, image } = await loadPinnedAgentServerOpenApi({
const {
schema: sourceSchema,
source,
image,
} = await loadPinnedAgentServerOpenApi({
repositoryRoot,
explicitSchemaPath,
});
const schema = pathPrefix ? selectPathPrefix(sourceSchema, pathPrefix) : sourceSchema;

const temporaryOutput = await mkdtemp(join(tmpdir(), 'agent-server-schema-'));
try {
Expand All @@ -34,12 +85,23 @@ async function main() {
});

const generated = await readFile(join(generatorOutput, 'types.gen.ts'), 'utf8');
const header = [
'// Generated file. Do not edit by hand.',
`// Source: ${explicitSchemaSource ?? image}`,
`// Regenerate with: npm run generate:agent-server-api`,
'',
].join('\n');
const header = pathPrefix
? [
'// Generated file. Do not edit by hand.',
`// Source: ${explicitSchemaSource ?? image} (paths matching ${pathPrefix})`,
'// Regenerate with:',
'// AGENT_SERVER_OPENAPI_PATH=/path/to/openapi.json',
`// AGENT_SERVER_OPENAPI_PATH_PREFIX=${pathPrefix}`,
'// AGENT_SERVER_GENERATED_OUTPUT=src/generated/prompt-enhancement-schema.ts',
`// npm run ${process.env.AGENT_SERVER_GENERATE_COMMAND ?? 'generate:agent-server-api'}`,
'',
].join('\n')
: [
'// Generated file. Do not edit by hand.',
`// Source: ${explicitSchemaSource ?? image}`,
`// Regenerate with: npm run generate:agent-server-api`,
'',
].join('\n');
const prettierConfig = (await resolveConfig(generatedFile)) ?? {};
const formatted = await format(`${header}${generated}`, {
...prettierConfig,
Expand Down
70 changes: 70 additions & 0 deletions clients/typescript/scripts/prompt-enhancement-api-schema.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { spawnSync } from 'node:child_process';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const typescriptRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repositoryRoot = resolve(typescriptRoot, '../..');
const generatedFile = join(typescriptRoot, 'src/generated/prompt-enhancement-schema.ts');
const exporter = join(repositoryRoot, '.github/scripts/export_agent_server_openapi.py');
const generator = join(typescriptRoot, 'scripts/generate-agent-server-api.mjs');
const write = process.argv[2] === '--write';

function run(command, args, options) {
const result = spawnSync(command, args, { stdio: 'inherit', ...options });
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`${command} exited with status ${result.status}`);
}

const temporaryDirectory = await mkdtemp(
join(dirname(generatedFile), '.prompt-enhancement-schema-')
);
try {
const openapiFile = join(temporaryDirectory, 'openapi.json');
const candidateFile = write ? generatedFile : join(temporaryDirectory, 'schema.ts');

run(
'uv',
[
'run',
'--frozen',
'--package',
'openhands-agent-server',
'python',
exporter,
'--output',
openapiFile,
],
{
cwd: repositoryRoot,
env: { ...process.env, OPENHANDS_SUPPRESS_BANNER: '1' },
}
);

run(process.execPath, [generator], {
cwd: typescriptRoot,
env: {
...process.env,
AGENT_SERVER_OPENAPI_PATH: openapiFile,
AGENT_SERVER_OPENAPI_PATH_PREFIX: '/api/prompt-enhancement/',
AGENT_SERVER_SCHEMA_SOURCE: 'current branch Agent Server OpenAPI',
AGENT_SERVER_GENERATE_COMMAND: 'generate:prompt-enhancement-api',
AGENT_SERVER_GENERATED_OUTPUT: candidateFile,
},
});

if (write) {
process.stdout.write(
'Generated prompt enhancement contract from current Agent Server source.\n'
);
} else if ((await readFile(generatedFile, 'utf8')) !== (await readFile(candidateFile, 'utf8'))) {
process.stderr.write(
'Prompt enhancement TypeScript contract is stale. Run npm run generate:prompt-enhancement-api.\n'
);
process.exitCode = 1;
} else {
process.stdout.write('Checked-in prompt enhancement contract is current.\n');
}
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
Loading
Loading