add-tws-feature
Skill400 starsChanged 9 days agoMITView on GitHub
Port a TWS API request, response, or field from the IBKR C# reference client into ibapi — read EClient/EDecoder, classify the request shape, register the message, encode/decode, type the fields, test, document. Use when adding a client method or a new proto field.
In this repository5 other files it gives its agents.
- AGENTS.mdAGENTS.md
- coverageSkill
- live-probeSkill
- releaseSkill
- shipSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Write these down before touching code:
- Very specific: line 39
Gate > 213 on a request: add the constant to src/server_versions.rs, a ProtocolFeature on Features (src/protocol.rs), and …
- Very specific: line 42
Gate > 213 on a field: check only when the caller sets it, beside its siblings.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Add a TWS API feature
Target: $ARGUMENTS. The worked example with code is
Extending the API (steps 1–9);
this skill is the order of work and the checks around it. Ship with /ship.
C# reference: tws-api/source/csharpclient/client/ in the IBKR TWS API distribution (the
maintainer's checkout: /Users/wboayue/projects/tws-api/). Protos: tws-api/source/proto/.
It is authoritative for encoding and version gates. It is not authoritative for semantics
(units, defaults, which values TWS accepts): for those, /live-probe.
1. Read the reference
Write these down before touching code:
| Find | Where in C# | Gives you |
Request method (reqFoo) and its version guard | EClient.cs: serverVersion < MinServerVer.X | Gate value (MinServerVer.cs) |
| Proto request builder | EClientUtils.cs: createFooRequestProto | Which fields are set, and when (e.g. transmit only if true) |
Cancel method (cancelFoo) | EClient.cs | Whether it's cancellable; IB sometimes reuses a related gate for cancels |
| Response handler(s) | EDecoder.cs: FooEventProtoBuf | Inbound message ids (IncomingMessage.cs), field order, end marker |
| Absent-field handling | EDecoder.cs / EDecoderUtils.cs | Early return = frame dropped; if (proto.HasX) = delivered with a default |
| Outgoing id | OutgoingMessages.cs | OutgoingMessages variant |
| Types | Contract.cs, Order.cs, ... | Plain string with no enum = likely free-form |
For algo parameters, the Testbed sample (samples/CSharp/Testbed/AvailableAlgoParams.cs)
is the wire spec; the docs page has conflated algos. For List<TagValue> options, protobuf
encodes a map<string,string>.
2. Gate and proto
- Gate ≤ 213 (
server_versions::PROTOBUF_REST_MESSAGES_3, the handshake floor): no check. - Gate > 213 on a request: add the constant to
src/server_versions.rs, aProtocolFeatureonFeatures(src/protocol.rs), andcheck_version(self.server_version(), Features::FOO)?before sending. - Gate > 213 on a field: check only when the caller sets it, beside its siblings. Order
fields: a local
ProtocolFeatureinorders/common/verify.rs(if order.conditions_include_overnight { check_version(..)? }). - Either way, tests derive the boundary from the constant (derive from constants).
- Gate > advertised max (
max_versioninsrc/connection/common.rs): raise it. TWS then starts sending every field gated between the old and new max, so check each against our decoders in the same PR (#1045 raised it to 226). - Proto missing from
src/proto/protobuf.rs:just protoregenerates it from IBKR's latest protos, so it can carry unrelated upstream changes. Read the regenerated diff: a changed or removed field elsewhere is its own change. Commit the regeneration on its own so the feature diff stays reviewable.
3. Classify the shape
| Shape | Signs in C# | Our pattern |
| One-shot keyed by request id | Request carries reqId, one answer | one_shot_by_request_id + expect_proto |
| One-shot, shared channel | No reqId in the answer | one_shot_shared; one_shot_shared_matching if frames can belong to another caller |
| Cancellable one-shot | One answer and a cancelFoo | one_shot_cancelling::<K> |
| Stream | Repeated frames, maybe an End, a cancelFoo | StreamDecoder + Subscription<T> |
| New field on an existing message | if (serverVersion >= X) in a builder/decoder | Extend the encoder/decoder and the domain type only |
Rules for each: one-shot narrowing, proto-only decoding. A stream's End marker comes after the initial dump only, not after every push (wire facts).
4. Implement
Order: types → message ids → registration → encoder → decoder → sync → async.
- Layout: the domain owns it: types in
<domain>/mod.rs(ungated), codecs in<domain>/common/{encoders,decoders}.rs,impl Clientin<domain>/{sync,async}.rs(domain module layout). Shared logic lives once incommon/, never copied across sync and async. - Registration: miss any of these and the stubbed tests stay green while a live gateway
hangs or drops frames:
IncomingMessages/OutgoingMessagesvariants insrc/messages.rstext_request_id_fieldinsrc/messages.rsfor every inbound type that carries a request id. It is the routing allow-list; without it the frame never reaches the subscription (proto-aware accessors)- one-shot: the payload in
impl_proto_payload!(src/proto/payload.rs) - stream: every handled type in
RESPONSE_MESSAGE_IDS, plus the decoder in the roster checked bydecoder_roster_is_complete - request ids come from
mint_request_id(), never a caller (id partition)
- Encoder helpers: reuse
ToField(src/lib.rs) andsome_str& co. (src/proto/encoders.rs); add missing ones there, not locally. MirrorEClientUtils's "set only when" conditions exactly. - Field typing (wire enum typing):
- Required (
Error::Parsewhen absent) only if C# drops the frame, or a default would fabricate a plausible wrong value. Otherwise tolerate:Option, or an open enum'sUnknown/Other(""). A decode error ends a long-lived stream. - Enum only after the fixtures or C# show a fixed vocabulary; on a streaming path, open
(
fallback Unknown).
- Required (
- Public API shape: three parameters or fewer, else a builder (param budget); one named setter per enum variant (builder enum coverage). Parameters every caller must pass go in the entry point, not a setter.
5. Test
- Fixtures in
src/testdata/builders/<domain>.rs, field-minimal, free-function entry points (fixture builders); tests in sibling*_tests.rsfiles (sibling test files). - Both sides: a sync and an async test through the real
impl Clientmethod, asserting the encoded request and the decoded result (exercise production code). - Version gate: one test below the constant (error) and one at it.
- An absent-field case per field:
Error::Parsefor required ones,None/ unset for the rest.
6. Document
- Rustdoc
# Exampleson each newpub fnand builder entry point, sync and async, each async example mirroring its sync sibling (public API examples). Check what a user copying it would see: an example that subscribes and then drops shows nothing. - Gate in the rustdoc ("Requires server version N"), and units where they apply.
- Runnable example in
examples/sync/andexamples/async/, each with an[[example]]entry inCargo.toml(required-features). Run it against the paper gateway once: stubbed tests can't catch a missing routing entry. CHANGELOG.mdunder## [Unreleased](changelog entry).
7. Verify live
An integration test in integration/{sync,async}/tests/<domain>.rs when the API has
observable behavior on a paper account (integration tests).
For anything C# didn't settle (units, accepted values, absent-field defaults), /live-probe,
and record the result in docs/wire-facts/.
Then /ship: wire changes also need the integration crate builds.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

